PackerTemplate.py 9 KB
Newer Older
1 2 3 4 5 6 7 8
import collections
import glob
import jinja2
import json
import os
import pprint
import yaml

9 10
from typing import List

11 12 13 14 15 16 17 18 19 20
class PackerTemplate:
    """This class implements one packer template.
    Basically, a packer template is made of four sections:
    - description: a string that explains the purpose of the template
    - variables: a dictionary of variables used in the template through jinja2 mechanism
    - builders: a list of dictionaries where each dictionary defines the type of image that will be built
    - provisioners: a list of dictionaries where each dictionary defines actions used to configure the image
    - processors: a list of dictionaries where each dictionary defines actions to be run after the image is built
    """

21
    def __init__(self, name: str, yaml_node: dict, packages: List[str]) -> None:
22 23
        """Constructor
        """
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54

        self._name = name

        self._parameters = yaml_node.get("parameters", {})

        # Fetch the 'packer' node
        packer_node = yaml_node.get("packer", {})

        # Fetch the 'description' node from the packer node
        self._description = packer_node.get("description", "No description provided")

        # Fetch the '_variables' node from the packer node
        self._variables = packer_node.get("variables", {})

        # Fetch the 'builders' node from the packer node
        self._builders = packer_node.get("builders", [])

        # Fetch the 'provisioners' node from the packer node
        self._provisioners = packer_node.get("provisioners", [])

        # Fetch the 'postprocessors' node from the packer node
        self._postprocessors = packer_node.get("postprocessors", [])

        bin_dir = os.path.realpath(os.path.dirname(__file__))
        root_dir = os.path.dirname(bin_dir)
        self._packages_root_dir = os.path.join(root_dir,"packages")
        self._templates_dir = os.path.join(root_dir,"templates")

        self._load_packages(packages)

    @property
55 56
    def builders(self) -> list:
        """Returns the list of packer builders of this :class:`PackerTemplate`.
57 58 59 60 61
        """

        return self._builders

    @property
62 63
    def description(self) -> str:
        """Returns the description of this :class:`PackerTemplate`.
64 65 66 67 68
        """

        return self._description

    @property
69 70
    def name(self) -> str:
        """Returns the name of this :class:`PackerTemplate`.
71 72 73 74 75
        """

        return self._name

    @property
76 77 78 79
    def parameters(self) -> dict:
        """Returns the parameters of this :class:`PackerTemplate`.
           
           This dictionary will be applied to Jinja 2 templates when creating the manifest json file.
80 81 82 83 84
        """

        return self._parameters

    @property
85 86
    def postprocessors(self) -> list:
        """Returns the postprocessors of this :class:`PackerTemplate`.
87 88 89 90 91
        """

        return self._postprocessors

    @property
92 93
    def provisioners(self) -> list:
        """Returns the provisioners of this :class:`PackerTemplate`.
94 95 96 97 98
        """

        return self._provisioners

    @property
99 100
    def variables(self) -> dict:
        """Returns the variables of this :class:`PackerTemplate`.
101 102 103 104
        """

        return self._variables

105 106
    def set_parent(self, parent_template: "PackerTemplate"):
        """Set the parent template to this :class:`PackerTemplate`.
107 108 109 110 111 112

        This defines a relationship for future packer run in the sense that the child template will start directly from the image of 
        its parent template.

        Parameters
        ----------
113 114
        parent_template: :class:`PackerTemplate`
            The :class:`PackerTemplate` of the parent template to connect the child template with.
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
            
        """

        # List of the builder names for the child template
        child_builder_names = [builder["name"] for builder in self._builders]

        # Loop over the builder of the parent template
        for builder in parent_template.builders:

            builder_name = builder["name"]

            # If this is a builder specific to the parent config, copy it in the child config and set it with an image dependency
            if builder_name not in child_builder_names:
                parent_builder = {}
                parent_builder["name"] = builder_name
                parent_builder["type"] = builder["type"]
                parent_builder["vm_name"] = self._name
                parent_builder["iso_url"] = "./builds/{}-{}".format(parent_template.name, builder_name)
                parent_builder["iso_checksum_type"] = "none"
                parent_builder["iso_checksum_url"] = "none"
135
                parent_builder["output_directory"] = os.path.join(self._templates_dir, self._name)
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
                self._builders.insert(0,parent_builder)

            # If the builder is also defined in the child config, use the child config one and specify the image dependency
            else:

                builder = next((b for b in self._builders if b["name"] == builder_name),None)
                if builder is None:
                    continue

                builder["vm_name"] = self._name
                builder["iso_url"] = "./builds/{}-{}".format(parent_template.name, builder_name)
                builder["iso_checksum_type"] = "none"
                builder["iso_checksum_url"] = "none"
                builder["output_directory"] = os.path.join(self._templates_dir,self._name)            

151 152
    def _load_packages(self, packages: List[str]):
        """Load the non-standard package YAML file and append them as provisioners of this :class:`PackerTemplate`.
153 154 155

        Parameters
        ----------
eric pellegrini's avatar
eric pellegrini committed
156
        list
157
            The non-standard packages to append.    
158 159 160 161 162 163 164 165 166
        """

        # If *" is in the list, fetch all the packages
        if "*" in packages:
            packages_dir = glob.glob(os.path.join(self._packages_root_dir,"*"))
        # Otherwise just fetch the selected ones
        else:
            packages_dir = []
            for package in packages:
eric pellegrini's avatar
eric pellegrini committed
167
                package_dir = os.path.join(self._packages_root_dir,package)
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
                if os.path.exists(package_dir) and os.path.isdir(package_dir):
                    packages_dir.append(package_dir)

        # Loop over the packages directories
        for package_dir in packages_dir:
            
            # Build the path to the package manifest file (YAML)
            manifest_file = os.path.join(package_dir,"manifest.yml")
            
            # Open and load the provisioners list from the manifest file
            try:
              fin = open(manifest_file, "r")
            # If the manifest does not exist, the provisioners list is set to an empty list
            except FileNotFoundError:
                manifest_data = []
            else:
184 185
                root_node = yaml.safe_load(fin)
                manifest_data = root_node["provisioners"]
186 187 188 189 190 191 192 193 194 195 196

            # Loop over the provisioners list and update when necessary relative paths with absolute one for packer to run correctly
            for provisioner in manifest_data:
                if provisioner["type"] in ["file"]:
                    provisioner["source"] = os.path.join(package_dir,provisioner["source"])
                elif provisioner["type"] in ["shell"]:
                    provisioner["script"] = os.path.join(package_dir,provisioner["script"])

            # Extend the current provisioners list with the ones of the selected packages
            self._provisioners.extend(manifest_data)
            
197
    def dump(self, output_file: str, **kwargs):
198 199 200 201 202
        """Dump this PackerTemplate to a file.

        Parameters
        ----------
        output_file: str
203
            The path to the output json file for this :class:`PackerTemplate`.
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
        """

        # Get the basename and the ext of the output_file
        basename, ext = os.path.splitext(output_file)
        # If the ext is different from .json set it to .json
        if ext != ".json":
            ext = ".json"

        # Reformat the output_file
        output_file = "{}{}".format(basename, ext)

        # This will be the node to be dumped
        node = {}
        node["description"] = self._description
        node["variables"] = self._variables
        node["builders"] = self._builders
        node["provisioners"] = self._provisioners
        node["post-processors"] = self._postprocessors

        # Render the jinja2 templates with the parameters dictionary provided in the template file and the available environment variables
        jinja_template = jinja2.Template(repr(node))
        s = jinja_template.render(parameters=self._parameters, environment=os.environ)

        # Dump to the output file
        with open(output_file, "w") as fout:
            json.dump(yaml.safe_load(s), fout, **kwargs)

231 232
    def __str__(self) -> str:
        """Returns the string representation for this :class:`PackerTemplate`.
233 234 235 236 237 238 239 240 241 242
        """

        d = collections.OrderedDict()
        d["description"] = self._description
        d["variables"] = self._variables
        d["builders"] = self._builders
        d["provisioners"] = self._provisioners
        d["post-processors"] = self._postprocessors

        return pprint.pformat(d)