template.py 11.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
"""
    Copyright (c) 2015 Ad Schellevis
    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

    1. Redistributions of source code must retain the above copyright notice,
     this list of conditions and the following disclaimer.

    2. Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.

    THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
    AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
    AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
    OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.

    --------------------------------------------------------------------------------------
27

28
    package : configd
29 30
    function: template handler, generate configuration files using templates
"""
31

32 33 34 35 36 37 38
__author__ = 'Ad Schellevis'

import os
import os.path
import collections
import copy
import jinja2
39
import addons.template_helpers
40

Ad Schellevis's avatar
Ad Schellevis committed
41

42 43
class Template(object):

Ad Schellevis's avatar
Ad Schellevis committed
44
    def __init__(self, target_root_directory="/"):
45 46 47
        """ constructor
        :return:
        """
48
        # init config (config.xml) data
49
        self._config = {}
50 51 52 53

        # set target root
        self._target_root_directory = target_root_directory

54 55
        # setup jinja2 environment
        self._template_dir = os.path.dirname(os.path.abspath(__file__))+'/../templates/'
56 57
        self._j2_env = jinja2.Environment(loader=jinja2.FileSystemLoader(self._template_dir), trim_blocks=True,
                                          extensions=["jinja2.ext.do",])
58

Ad Schellevis's avatar
Ad Schellevis committed
59
    def _readManifest(self, filename):
60 61 62 63 64 65
        """

        :param filename: manifest filename (path/+MANIFEST)
        :return: dictionary containing manifest items
        """
        result = {}
Ad Schellevis's avatar
Ad Schellevis committed
66 67
        for line in open(filename, 'r').read().split('\n'):
            parts = line.split(':')
68 69 70 71 72
            if len(parts) > 1:
                result[parts[0]] = ':'.join(parts[1:])

        return result

Ad Schellevis's avatar
Ad Schellevis committed
73
    def _readTargets(self, filename):
74 75 76 77 78 79
        """ read raw target filename masks

        :param filename: targets filename (path/+TARGETS)
        :return: dictionary containing +TARGETS filename sets
        """
        result = {}
Ad Schellevis's avatar
Ad Schellevis committed
80 81
        for line in open(filename, 'r').read().split('\n'):
            parts = line.split(':')
82 83 84 85 86
            if len(parts) > 1 and parts[0].strip()[0] != '#':
                result[parts[0]] = ':'.join(parts[1:]).strip()

        return result

Ad Schellevis's avatar
Ad Schellevis committed
87
    def list_module(self, module_name, read_manifest=False):
88 89 90 91 92 93
        """ list single module content
        :param module_name: module name in dot notation ( company.module )
        :param read_manifest: boolean, read manifest file if it exists
        :return: dictionary with module data
        """
        result = {}
Ad Schellevis's avatar
Ad Schellevis committed
94 95 96 97 98
        file_path = '%s/%s' % (self._template_dir, module_name.replace('.', '/'))
        if os.path.exists('%s/+MANIFEST' % file_path) and read_manifest:
            result['+MANIFEST'] = self._readManifest('%s/+MANIFEST' % file_path)
        if os.path.exists('%s/+TARGETS' % file_path):
            result['+TARGETS'] = self._readTargets('%s/+TARGETS' % file_path)
99 100 101 102 103
        else:
            result['+TARGETS'] = {}

        return result

Ad Schellevis's avatar
Ad Schellevis committed
104
    def list_modules(self):
105 106 107 108 109 110 111 112
        """ traverse template directory and list all modules
        the template directory is structured like Manufacturer/Module/config_files

        :return: list (dict) of registered modules
        """
        result = {}
        for root, dirs, files in os.walk(self._template_dir):
            if len(root) > len(self._template_dir):
Ad Schellevis's avatar
Ad Schellevis committed
113 114
                module_name = '.'.join(root.replace(self._template_dir, '').split('/')[:2])
                if module_name not in result:
115 116 117 118
                    result[module_name] = self.list_module(module_name)

        return result

Ad Schellevis's avatar
Ad Schellevis committed
119
    def setConfig(self, config_data):
120 121 122 123
        """ set config data
        :param config_data: config data as dictionary/list structure
        :return: None
        """
Ad Schellevis's avatar
Ad Schellevis committed
124
        if type(config_data) in(dict, collections.OrderedDict):
125 126 127 128 129
            self._config = config_data
        else:
            # no data given, reset
            self._config = {}

Ad Schellevis's avatar
Ad Schellevis committed
130
    def __findStringTags(self, instr):
131 132 133 134 135 136 137 138 139 140 141
        """
        :param instr: string with optional tags [field.$$]
        :return:
        """
        retval = []
        for item in instr.split('['):
            if item.find(']') > -1:
                retval.append(item.split(']')[0])

        return retval

Ad Schellevis's avatar
Ad Schellevis committed
142
    def __findFilters(self, tags):
143 144 145 146 147 148 149 150 151 152 153 154
        """ match tags to config and construct a dictionary which we can use to construct the output filenames
        :param tags: list of tags [xmlnode.xmlnode.%.xmlnode,xmlnode]
        :return: dictionary containing key (tagname) value {existing node key, value}
        """
        result = {}
        for tag in tags:
            result[tag] = {}
            # first step, find wildcard to replace ( if any )
            # ! we only support one wildcard per tag at the moment, should be enough for most situations
            config_ptr = self._config
            target_keys = []
            for xmlNodeName in tag.split('.'):
Ad Schellevis's avatar
Ad Schellevis committed
155
                if xmlNodeName in config_ptr:
156 157
                    config_ptr = config_ptr[xmlNodeName]
                elif xmlNodeName == '%':
158 159 160 161
                    if type(config_ptr) in (collections.OrderedDict, dict):
                        target_keys = config_ptr.keys()
                    else:
                        target_keys = map(lambda x: str(x), range(len(config_ptr)))
162 163 164
                else:
                    break

Ad Schellevis's avatar
Ad Schellevis committed
165
            if len(target_keys) == 0:
166
                # single node, only used for string replacement in output name.
Ad Schellevis's avatar
Ad Schellevis committed
167
                result[tag] = {tag: config_ptr}
168 169 170 171 172
            else:
                # multiple node's, find all nodes
                for target_node in target_keys:
                    config_ptr = self._config
                    str_wildcard_loc = len(tag.split('%')[0].split('.'))
Ad Schellevis's avatar
Ad Schellevis committed
173 174 175 176
                    filter_target = []
                    for xmlNodeName in tag.replace('%', target_node).split('.'):
                        if xmlNodeName in config_ptr:
                            if type(config_ptr[xmlNodeName]) in (collections.OrderedDict, dict):
177 178 179 180 181 182
                                if str_wildcard_loc >= len(filter_target):
                                    filter_target.append(xmlNodeName)
                                if str_wildcard_loc == len(filter_target):
                                    result[tag]['.'.join(filter_target)] = xmlNodeName

                                config_ptr = config_ptr[xmlNodeName]
183 184 185 186 187
                            elif type(config_ptr[xmlNodeName]) in (list, tuple):
                                if str_wildcard_loc >= len(filter_target):
                                    filter_target.append(xmlNodeName)
                                    filter_target.append(target_node)
                                config_ptr = config_ptr[xmlNodeName][int(target_node)]
188 189 190 191 192 193
                            else:
                                # fill in node value
                                result[tag]['.'.join(filter_target)] = config_ptr[xmlNodeName]

        return result

Ad Schellevis's avatar
Ad Schellevis committed
194
    def _create_directory(self, filename):
195 196 197 198
        """ create directory
        :param filename: create path for filename ( if not existing )
        :return: None
        """
Ad Schellevis's avatar
Ad Schellevis committed
199
        fparts = []
200 201
        for fpart in filename.strip().split('/')[:-1]:
            fparts.append(fpart)
Ad Schellevis's avatar
Ad Schellevis committed
202 203
            if len(fpart) > 1:
                if not os.path.exists('/'.join(fparts)):
204 205
                    os.mkdir('/'.join(fparts))

Ad Schellevis's avatar
Ad Schellevis committed
206
    def generate(self, module_name, create_directory=True):
207 208 209 210 211 212
        """ generate configuration files using bound config and template data

        :param module_name: module name in dot notation ( company.module )
        :param create_directory: automatically create directories to place template output in ( if not existing )
        :return: list of generated output files
        """
Ad Schellevis's avatar
Ad Schellevis committed
213
        result = []
214 215
        module_data = self.list_module(module_name)
        for src_template in module_data['+TARGETS'].keys():
Ad Schellevis's avatar
Ad Schellevis committed
216
            target = module_data['+TARGETS'][src_template]
217 218 219

            target_filename_tags = self.__findStringTags(target)
            target_filters = self.__findFilters(target_filename_tags)
Ad Schellevis's avatar
Ad Schellevis committed
220
            result_filenames = {target: {}}
221 222 223
            for target_filter in target_filters.keys():
                for key in target_filters[target_filter].keys():
                    for filename in result_filenames.keys():
Ad Schellevis's avatar
Ad Schellevis committed
224 225
                        if filename.find('[%s]' % target_filter) > -1:
                            new_filename = filename.replace('[%s]' % target_filter, target_filters[target_filter][key])
226 227 228
                            result_filenames[new_filename] = copy.deepcopy(result_filenames[filename])
                            result_filenames[new_filename][key] = target_filters[target_filter][key]

229 230
            template_filename = '%s/%s'%(module_name.replace('.', '/'), src_template)
            j2_page = self._j2_env.get_template(template_filename)
231
            for filename in result_filenames.keys():
Ad Schellevis's avatar
Ad Schellevis committed
232
                if not (filename.find('[') != -1 and filename.find(']') != -1):
233 234 235 236
                    # copy config data
                    cnf_data = copy.deepcopy(self._config)
                    cnf_data['TARGET_FILTERS'] = result_filenames[filename]

237 238 239
                    # link template helpers
                    self._j2_env.globals['helpers'] = addons.template_helpers.Helpers(cnf_data)

240 241 242 243 244
                    # make sure we're only rendering output once
                    if filename not in result:
                        # render page and write to disc
                        content = j2_page.render(cnf_data)

245
                        # prefix filename with defined root directory
Ad Schellevis's avatar
Ad Schellevis committed
246
                        filename = ('%s/%s' % (self._target_root_directory, filename)).replace('//', '/')
247 248 249 250
                        if create_directory:
                            # make sure the target directory exists
                            self._create_directory(filename)

Ad Schellevis's avatar
Ad Schellevis committed
251
                        f_out = open(filename, 'wb')
252
                        f_out.write(content)
253 254 255
                        # Check if the last character of our output contains an end-of-line, if not copy it in if
                        # it was in the original template.
                        # It looks like Jinja sometimes isn't consistent on placing this last end-of-line in.
256
                        if len(content) > 1 and content[-1] != '\n':
257 258 259 260 261 262 263
                            src_file = '%s%s'%(self._template_dir,template_filename)
                            src_file_handle = open(src_file,'r')
                            src_file_handle.seek(-1, os.SEEK_END)
                            last_bytes_template = src_file_handle.read()
                            src_file_handle.close()
                            if last_bytes_template in ('\n', '\r'):
                                f_out.write('\n')
264 265 266 267 268
                        f_out.close()

                        result.append(filename)

        return result