Completed
Pull Request — master (#6432)
by Matěj
04:20 queued 02:11
created

ssg.templates.Builder.get_langs_to_generate()   B

Complexity

Conditions 6

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 15
nop 2
dl 0
loc 21
ccs 0
cts 13
cp 0
crap 42
rs 8.6666
c 0
b 0
f 0
1
from __future__ import absolute_import
2
from __future__ import print_function
3
4
import os
5
import sys
6
import imp
7
import glob
8
9
import ssg.build_yaml
10
import ssg.utils
11
import ssg.yaml
12
13
try:
14
    from urllib.parse import quote
15
except ImportError:
16
    from urllib import quote
17
18
languages = ["anaconda", "ansible", "bash", "oval", "puppet", "ignition", "kubernetes"]
19
preprocessing_file_name = "template.py"
20
lang_to_ext_map = {
21
    "anaconda": ".anaconda",
22
    "ansible": ".yml",
23
    "bash": ".sh",
24
    "oval": ".xml",
25
    "puppet": ".pp",
26
    "ignition": ".yml",
27
    "kubernetes": ".yml"
28
}
29
30
31
templates = dict()
32
33
34
class Template():
35
    def __init__(self, template_root_directory, name):
36
        self.template_root_directory = template_root_directory
37
        self.name = name
38
        self.template_path = os.path.join(self.template_root_directory, self.name)
39
        self.template_yaml_path = os.path.join(self.template_path, "template.yml")
40
        self.preprocessing_file_path = os.path.join(self.template_path, preprocessing_file_name)
41
42
    def load(self):
43
        if not os.path.exists(self.preprocessing_file_path):
44
            self.preprocessing_file_path = None
45
        self.langs = []
46
        template_yaml = ssg.yaml.open_raw(self.template_yaml_path)
47
        for lang in template_yaml["supported_languages"]:
48
            if lang not in languages:
49
                raise ValueError("The template {0} declares to support the {1} language,"
50
                "but this language is not supported by the content.".format(self.name, lang))
51
            langfilename = lang + ".template"
52
            if not os.path.exists(os.path.join(self.template_path, langfilename)):
53
                raise ValueError("The template {0} declares to support the {1} language,"
54
                "but the implementation file is missing.".format(self.name, lang))
55
            self.langs.append(lang)
56
57
    def preprocess(self, parameters, lang):
58
        # if no template.py file exists, skip this preprocessing part
59
        if self.preprocessing_file_path is not None:
60
            module_name = "tmpmod" # dummy module name, we don't need it later
61
            preprocess_mod = imp.load_source(module_name, self.preprocessing_file_path)
62
            parameters = preprocess_mod.preprocess(parameters.copy(), lang)
63
        # TODO: Remove this right after the variables in templates are renamed
64
        # to lowercase
65
        uppercases = dict()
66
        for k, v in parameters.items():
67
            uppercases[k.upper()] = v
68
        return uppercases
69
70
    def looks_like_template(self):
71
        if not os.path.isdir(self.template_root_directory):
72
            return False
73
        if os.path.islink(self.template_root_directory):
74
            return False
75
        template_sources = glob.glob(os.path.join(self.template_path, "*.template"))
76
        if not os.path.isfile(self.template_yaml_path) and not template_sources:
77
            return False
78
        return True
79
80
81
class Builder(object):
82
    """
83
    Class for building all templated content for a given product.
84
85
    To generate content from templates, pass the env_yaml, path to the
86
    directory with resolved rule YAMLs, path to the directory that contains
87
    templates, path to the output directory for checks and a path to the
88
    output directory for remediations into the constructor. Then, call the
89
    method build() to perform a build.
90
    """
91
    def __init__(
92
            self, env_yaml, resolved_rules_dir, templates_dir,
93
            remediations_dir, checks_dir):
94
        self.env_yaml = env_yaml
95
        self.resolved_rules_dir = resolved_rules_dir
96
        self.templates_dir = templates_dir
97
        self.remediations_dir = remediations_dir
98
        self.checks_dir = checks_dir
99
        self.output_dirs = dict()
100
        for lang in languages:
101
            if lang == "oval":
102
                # OVAL checks need to be put to a different directory because
103
                # they are processed differently than remediations later in the
104
                # build process
105
                output_dir = self.checks_dir
106
            else:
107
                output_dir = self.remediations_dir
108
            dir_ = os.path.join(output_dir, lang)
109
            self.output_dirs[lang] = dir_
110
        # scan directory structure and dynamically create list of templates
111
        for item in os.listdir(self.templates_dir):
112
            itempath = os.path.join(self.templates_dir, item)
113
            maybe_template = Template(templates_dir, item)
114
            if maybe_template.looks_like_template():
115
                maybe_template.load()
116
                templates[item] = maybe_template
117
118
119
    def build_lang(
120
            self, rule_id, template_name, template_vars, lang, local_env_yaml):
121
        """
122
        Builds templated content for a given rule for a given language.
123
        Writes the output to the correct build directories.
124
        """
125
        if lang not in templates[template_name].langs:
126
            return
127
        template_file_name = lang + ".template"
128
        template_file_path = os.path.join(self.templates_dir, template_name, template_file_name)
129
        ext = lang_to_ext_map[lang]
130
        output_file_name = rule_id + ext
131
        output_filepath = os.path.join(
132
            self.output_dirs[lang], output_file_name)
133
        template_parameters = templates[template_name].preprocess(template_vars, lang)
134
        jinja_dict = ssg.utils.merge_dicts(local_env_yaml, template_parameters)
135
        filled_template = ssg.jinja.process_file_with_macros(
136
            template_file_path, jinja_dict)
137
        with open(output_filepath, "w") as f:
138
            f.write(filled_template)
139
140
    def get_langs_to_generate(self, rule):
141
        """
142
        For a given rule returns list of languages that should be generated
143
        from templates. This is controlled by "template_backends" in rule.yml.
144
        """
145
        if "backends" in rule.template:
146
            backends = rule.template["backends"]
147
            for lang in backends:
148
                if lang not in languages:
149
                    raise RuntimeError(
150
                        "Rule {0} wants to generate unknown language '{1}"
151
                        "from a template.".format(rule.id_, lang)
152
                    )
153
            langs_to_generate = []
154
            for lang in languages:
155
                backend = backends.get(lang, "on")
156
                if backend == "on":
157
                    langs_to_generate.append(lang)
158
            return langs_to_generate
159
        else:
160
            return languages
161
162
    def build_rule(self, rule_id, rule_title, template, langs_to_generate):
163
        """
164
        Builds templated content for a given rule for selected languages,
165
        writing the output to the correct build directories.
166
        """
167
        try:
168
            template_name = template["name"]
169
        except KeyError:
170
            raise ValueError(
171
                "Rule {0} is missing template name under template key".format(
172
                    rule_id))
173
        if template_name not in templates.keys():
174
            raise ValueError(
175
                "Rule {0} uses template {1} which does not exist.".format(
176
                    rule_id, template_name))
177
        try:
178
            template_vars = template["vars"]
179
        except KeyError:
180
            raise ValueError(
181
                "Rule {0} does not contain mandatory 'vars:' key under "
182
                "'template:' key.".format(rule_id))
183
        # Add the rule ID which will be reused in OVAL templates as OVAL
184
        # definition ID so that the build system matches the generated
185
        # check with the rule.
186
        template_vars["_rule_id"] = rule_id
187
        # checks and remediations are processed with a custom YAML dict
188
        local_env_yaml = self.env_yaml.copy()
189
        local_env_yaml["rule_id"] = rule_id
190
        local_env_yaml["rule_title"] = rule_title
191
        local_env_yaml["products"] = self.env_yaml["product"]
192
        for lang in langs_to_generate:
193
            self.build_lang(
194
                rule_id, template_name, template_vars, lang, local_env_yaml)
195
196
    def build_extra_ovals(self):
197
        declaration_path = os.path.join(self.templates_dir, "extra_ovals.yml")
198
        declaration = ssg.yaml.open_raw(declaration_path)
199
        for oval_def_id, template in declaration.items():
200
            langs_to_generate = ["oval"]
201
            # Since OVAL definition ID in shorthand format is always the same
202
            # as rule ID, we can use it instead of the rule ID even if no rule
203
            # with that ID exists
204
            self.build_rule(
205
                oval_def_id, oval_def_id, template, langs_to_generate)
206
207
    def build_all_rules(self):
208
        for rule_file in os.listdir(self.resolved_rules_dir):
209
            rule_path = os.path.join(self.resolved_rules_dir, rule_file)
210
            try:
211
                rule = ssg.build_yaml.Rule.from_yaml(rule_path, self.env_yaml)
212
            except ssg.build_yaml.DocumentationNotComplete:
213
                # Happens on non-debug build when a rule is "documentation-incomplete"
214
                continue
215
            if rule.template is None:
216
                # rule is not templated, skipping
217
                continue
218
            langs_to_generate = self.get_langs_to_generate(rule)
219
            self.build_rule(
220
                rule.id_, rule.title, rule.template, langs_to_generate)
221
222
    def build(self):
223
        """
224
        Builds all templated content for all languages, writing
225
        the output to the correct build directories.
226
        """
227
228
        for dir_ in self.output_dirs.values():
229
            if not os.path.exists(dir_):
230
                os.makedirs(dir_)
231
232
        self.build_extra_ovals()
233
        self.build_all_rules()
234