diff --git a/src/opnsense/service/conf/actions_template.conf b/src/opnsense/service/conf/actions_template.conf new file mode 100644 index 000000000..409b85ce6 --- /dev/null +++ b/src/opnsense/service/conf/actions_template.conf @@ -0,0 +1,6 @@ +[reload] +command:template.reload +parameters:%s +type:inline +message:generate template %s +config:/conf/config.xml diff --git a/src/opnsense/service/execute_command.py b/src/opnsense/service/execute_command.py index 4b4aa9d37..717440505 100755 --- a/src/opnsense/service/execute_command.py +++ b/src/opnsense/service/execute_command.py @@ -2,7 +2,7 @@ """ Copyright (c) 2014 Ad Schellevis - part of opnSense (https://www.opnsense.org/) + part of OPNsense (https://www.opnsense.org/) All rights reserved. @@ -61,7 +61,11 @@ except socket.error, msg: # send command and await response try: + print ('send:%s '%exec_command) sock.send(exec_command) print ('response:%s'% sock.recv(4096)) finally: sock.close() + + + diff --git a/src/opnsense/service/modules/__init__.py b/src/opnsense/service/modules/__init__.py index 987a98c62..2f428d884 100644 --- a/src/opnsense/service/modules/__init__.py +++ b/src/opnsense/service/modules/__init__.py @@ -30,4 +30,4 @@ -""" +""" \ No newline at end of file diff --git a/src/opnsense/service/modules/config.py b/src/opnsense/service/modules/config.py new file mode 100644 index 000000000..9a9d95bda --- /dev/null +++ b/src/opnsense/service/modules/config.py @@ -0,0 +1,122 @@ +""" + Copyright (c) 2015 Ad Schellevis + + part of OPNsense (https://www.opnsense.org/) + + 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. + + -------------------------------------------------------------------------------------- + package : check_reload_status + function: config handler + + + +""" +__author__ = 'Ad Schellevis' + +import os +import stat +import collections +import copy +import xml.etree.cElementTree as ElementTree + + +class Config(object): + def __init__(self,filename): + self._filename = filename + self._config_data = {} + self._file_mod = 0 + + self._load() + + def _load(self): + """ load config ( if timestamp is changed ) + + :return: + """ + mod_time = os.stat(self._filename)[stat.ST_MTIME] + if self._file_mod != mod_time: + xml_node = ElementTree.parse(self._filename) + root = xml_node.getroot() + self._config_data = self._traverse(root) + self._file_mod = mod_time + + def _traverse(self,xmlNode): + """ traverse xml node and return ordered dictionary structure + :param xmlNode: ElementTree node + :return: collections.OrderedDict + """ + this_item = collections.OrderedDict() + if len(list(xmlNode)) > 0 : + for item in list(xmlNode): + item_content = self._traverse(item) + if this_item.has_key(item.tag): + if type(this_item[item.tag]) != list: + tmp_item = copy.deepcopy(this_item[item.tag]) + this_item[item.tag] = [] + this_item[item.tag].append(tmp_item) + + if item_content != None: + # skip empty fields + this_item[item.tag].append(item_content) + elif item_content != None: + # create a new named item + this_item[item.tag] = self._traverse(item) + else: + # last node, return text + return xmlNode.text + + return this_item + + + def indent(self,elem, level=0): + """ indent cElementTree (prettyprint fix) + used from : http://infix.se/2007/02/06/gentlemen-indent-your-xml + @param elem: cElementTree + @param level: Currentlevel + """ + i = "\n" + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + for e in elem: + self.indent(e, level+1) + if not e.tail or not e.tail.strip(): + e.tail = i + " " + if not e.tail or not e.tail.strip(): + e.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + + def get(self): + """ get active config data, load from disc if file in memory is different + + :return: dictionary + """ + # refresh config if source xml is changed + self._load() + + return self._config_data \ No newline at end of file diff --git a/src/opnsense/service/modules/ph_inline_actions.py b/src/opnsense/service/modules/ph_inline_actions.py new file mode 100644 index 000000000..f10d51063 --- /dev/null +++ b/src/opnsense/service/modules/ph_inline_actions.py @@ -0,0 +1,64 @@ +""" + Copyright (c) 2014 Ad Schellevis + + part of OPNsense (https://www.opnsense.org/) + + 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. + + -------------------------------------------------------------------------------------- + package : check_reload_status + function: unix domain socket process worker process + + +""" +__author__ = 'Ad Schellevis' + +import syslog + + +def execute(action,parameters): + """ wrapper for inline functions + + :param action: action object ( processhandler.Action type ) + :param parameters: parameter string + :return: status ( string ) + """ + if action.command == 'template.reload': + import template + import config + tmpl = template.Template() + conf = config.Config(action.config) + tmpl.setConfig(conf.get()) + filenames = tmpl.generate(parameters) + + # send generated filenames to syslog + for filename in filenames: + syslog.syslog(syslog.LOG_DEBUG,' %s generated %s' % ( parameters, filename ) ) + + del conf + del tmpl + + return 'OK' + + return 'ERR' \ No newline at end of file diff --git a/src/opnsense/service/modules/processhandler.py b/src/opnsense/service/modules/processhandler.py index 2981a7e1d..100e66ec8 100644 --- a/src/opnsense/service/modules/processhandler.py +++ b/src/opnsense/service/modules/processhandler.py @@ -1,7 +1,7 @@ """ Copyright (c) 2014 Ad Schellevis - part of opnSense (https://www.opnsense.org/) + part of OPNsense (https://www.opnsense.org/) All rights reserved. @@ -43,6 +43,7 @@ import threading import ConfigParser import glob import time +import ph_inline_actions class Handler(object): """ Main handler class, opens unix domain socket and starts listening @@ -52,7 +53,7 @@ class Handler(object): processflow: Handler ( waits for client ) -> new client is send to HandlerClient - -> execute ActionHandler command + -> execute ActionHandler command using Action objects <- send back result string """ def __init__(self,socket_filename,config_path,simulation_mode=False): @@ -164,77 +165,6 @@ class HandlerClient(threading.Thread): finally: self.connection.close() -class Action(object): - """ Action class, handles actual system calls. set command, parameters (template) type and log message - """ - def __init__(self): - """ setup default properties - - :return: - """ - self.command = None - self.parameters = None - self.type = None - self.message = None - self._parameter_start_pos = 0 - - def setParameterStartPos(self,pos): - """ - - :param pos: start position of parameter list - :return: position - """ - self._parameter_start_pos = pos - - def getParameterStartPos(self): - """ getter for _parameter_start_pos - :return: start position of parameter list ( first argument can be part of action to start ) - """ - return self._parameter_start_pos - - def execute(self,parameters): - """ execute an action - - :param parameters: list of parameters - :return: - """ - # validate input - if self.type == None: - return 'No action type' - elif self.type.lower() == 'script': - if self.command == None: - return 'No command' - - try: - script_command = self.command - if self.parameters != None and type(self.parameters) == str: - script_command = '%s %s'%(script_command,self.parameters) - - if script_command.find('%s') > -1 and len(parameters) > 0: - # use command execution parameters in action parameter template - script_command = script_command % tuple(parameters[0:script_command.count('%s')]) - - # execute script command - if self.message != None: - if self.message.count('%s') > 0 and parameters != None and len(parameters) > 0: - syslog.syslog(syslog.LOG_NOTICE,self.message % tuple(parameters[0:self.message.count('%s')]) ) - else: - syslog.syslog(syslog.LOG_NOTICE,self.message) - - exit_status = subprocess.call(script_command, shell=True) - except: - print traceback.format_exc() - # todo log traceback on exception - return 'Execute error' - - # send response - if exit_status == 0 : - return 'OK' - else: - return 'Error (%d)'%exit_status - - return 'Unknown action type' - class ActionHandler(object): """ Start/stop services and functions using configuration data definced in conf/actions_.conf """ @@ -330,3 +260,100 @@ class ActionHandler(object): print ('execute %s.%s with parameters : %s '%(command,action,parameters) ) print ('action object %s (%s)' % (action_obj,action_obj.command) ) print ('---------------------------------------------------------------------') + +class Action(object): + """ Action class, handles actual (system) calls. + set command, parameters (template) type and log message + """ + def __init__(self): + """ setup default properties + + :return: + """ + self.command = None + self.parameters = None + self.type = None + self.message = None + self._parameter_start_pos = 0 + + def setParameterStartPos(self,pos): + """ + + :param pos: start position of parameter list + :return: position + """ + self._parameter_start_pos = pos + + def getParameterStartPos(self): + """ getter for _parameter_start_pos + :return: start position of parameter list ( first argument can be part of action to start ) + """ + return self._parameter_start_pos + + def execute(self,parameters): + """ execute an action + + :param parameters: list of parameters + :return: + """ + # validate input + if self.type == None: + return 'No action type' + elif self.type.lower() == 'script': + # + # script command, execute a shell script and return (simple) status + # + if self.command == None: + return 'No command' + try: + script_command = self.command + if self.parameters != None and type(self.parameters) == str: + script_command = '%s %s'%(script_command,self.parameters) + + if script_command.find('%s') > -1 and len(parameters) > 0: + # use command execution parameters in action parameter template + script_command = script_command % tuple(parameters[0:script_command.count('%s')]) + + # execute script command + if self.message != None: + if self.message.count('%s') > 0 and parameters != None and len(parameters) > 0: + syslog.syslog(syslog.LOG_NOTICE,self.message % tuple(parameters[0:self.message.count('%s')]) ) + else: + syslog.syslog(syslog.LOG_NOTICE,self.message) + + exit_status = subprocess.call(script_command, shell=True) + except: + syslog.syslog(syslog.LOG_ERR, 'Script action failed at %s'%traceback.format_exc()) + return 'Execute error' + + # send response + if exit_status == 0 : + return 'OK' + else: + return 'Error (%d)'%exit_status + + elif self.type.lower() == 'inline': + # Handle inline service actions + try: + # match parameters, serialize to parameter string defined by action template + if len(parameters) > 0: + inline_act_parameters = self.parameters % tuple(parameters) + else: + inline_act_parameters = '' + + # send message to syslog + if self.message != None: + if self.message.count('%s') > 0 and parameters != None and len(parameters) > 0: + syslog.syslog(syslog.LOG_NOTICE,self.message % tuple(parameters[0:self.message.count('%s')]) ) + else: + syslog.syslog(syslog.LOG_NOTICE,self.message) + + return ph_inline_actions.execute(self,inline_act_parameters) + + except: + syslog.syslog(syslog.LOG_ERR, 'Inline action failed at %s'%traceback.format_exc()) + return 'Execute error' + + + + return 'Unknown action type' diff --git a/src/opnsense/service/modules/template.py b/src/opnsense/service/modules/template.py new file mode 100644 index 000000000..35f28ea1c --- /dev/null +++ b/src/opnsense/service/modules/template.py @@ -0,0 +1,246 @@ +""" + Copyright (c) 2015 Ad Schellevis + + part of OPNsense (https://www.opnsense.org/) + + 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. + + -------------------------------------------------------------------------------------- + package : check_reload_status + function: template handler, generate configuration files using templates + + +""" +__author__ = 'Ad Schellevis' + +import os +import os.path +import collections +import copy +import jinja2 + +class Template(object): + + def __init__(self): + """ constructor + :return: + """ + self._config = {} + # setup jinja2 environment + self._template_dir = os.path.dirname(os.path.abspath(__file__))+'/../templates/' + self._j2_env = jinja2.Environment(loader=jinja2.FileSystemLoader(self._template_dir),trim_blocks=True) + + def _readManifest(self,filename): + """ + + :param filename: manifest filename (path/+MANIFEST) + :return: dictionary containing manifest items + """ + result = {} + for line in open(filename,'r').read().split('\n'): + parts = line.split(':') + if len(parts) > 1: + result[parts[0]] = ':'.join(parts[1:]) + + return result + + def _readTargets(self,filename): + """ read raw target filename masks + + :param filename: targets filename (path/+TARGETS) + :return: dictionary containing +TARGETS filename sets + """ + result = {} + for line in open(filename,'r').read().split('\n'): + parts = line.split(':') + if len(parts) > 1 and parts[0].strip()[0] != '#': + result[parts[0]] = ':'.join(parts[1:]).strip() + + return result + + def list_module(self,module_name,read_manifest=False): + """ 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 = {} + 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) + else: + result['+TARGETS'] = {} + + return result + + def list_modules(self,read_manifest=False): + """ traverse template directory and list all modules + the template directory is structured like Manufacturer/Module/config_files + + :param read_manifest: boolean, read manifest file if it exists + :return: list (dict) of registered modules + """ + result = {} + for root, dirs, files in os.walk(self._template_dir): + if len(root) > len(self._template_dir): + module_name = '.'.join(root.replace(self._template_dir,'').split('/')[:2]) + if result.has_key(module_name) == False: + result[module_name] = self.list_module(module_name) + + return result + + + def setConfig(self,config_data): + """ set config data + :param config_data: config data as dictionary/list structure + :return: None + """ + if type(config_data) in( dict, collections.OrderedDict): + self._config = config_data + else: + # no data given, reset + self._config = {} + + def __findStringTags(self,instr): + """ + :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 + + def __findFilters(self,tags): + """ 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('.'): + if config_ptr.has_key(xmlNodeName): + config_ptr = config_ptr[xmlNodeName] + elif xmlNodeName == '%': + target_keys = config_ptr.keys() + else: + break + + if len(target_keys) == 0 : + # single node, only used for string replacement in output name. + result[tag] = {tag:config_ptr} + 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('.')) + filter_target= [] + for xmlNodeName in tag.replace('%',target_node).split('.'): + if config_ptr.has_key(xmlNodeName): + if type (config_ptr[xmlNodeName]) in (collections.OrderedDict,dict): + 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] + else: + # fill in node value + result[tag]['.'.join(filter_target)] = config_ptr[xmlNodeName] + + return result + + def _create_directory(self,filename): + """ create directory + :param filename: create path for filename ( if not existing ) + :return: None + """ + fparts=[] + for fpart in filename.strip().split('/')[:-1]: + fparts.append(fpart) + if len(fpart) >1: + if os.path.exists('/'.join(fparts)) == False: + os.mkdir('/'.join(fparts)) + + + + def generate(self,module_name,create_directory=True): + """ 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 + """ + result=[] + module_data = self.list_module(module_name) + for src_template in module_data['+TARGETS'].keys(): + target = module_data['+TARGETS'][src_template] + + target_filename_tags = self.__findStringTags(target) + target_filters = self.__findFilters(target_filename_tags) + result_filenames = {target:{}} + for target_filter in target_filters.keys(): + for key in target_filters[target_filter].keys(): + for filename in result_filenames.keys(): + if filename.find('[%s]'%target_filter) > -1: + new_filename = filename.replace('[%s]'%target_filter,target_filters[target_filter][key]) + result_filenames[new_filename] = copy.deepcopy(result_filenames[filename]) + result_filenames[new_filename][key] = target_filters[target_filter][key] + + j2_page = self._j2_env.get_template('%s/%s'%( module_name.replace('.','/'), src_template)) + for filename in result_filenames.keys(): + if not ( filename.find('[') != -1 and filename.find(']') != -1 ) : + # copy config data + cnf_data = copy.deepcopy(self._config) + cnf_data['TARGET_FILTERS'] = result_filenames[filename] + + # 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) + + if create_directory: + # make sure the target directory exists + self._create_directory(filename) + + f_out = open(filename,'wb') + f_out.write(content) + f_out.close() + + result.append(filename) + + + return result + + + diff --git a/src/opnsense/service/templates/OPNsense/Sample/+MANIFEST b/src/opnsense/service/templates/OPNsense/Sample/+MANIFEST new file mode 100644 index 000000000..72083934b --- /dev/null +++ b/src/opnsense/service/templates/OPNsense/Sample/+MANIFEST @@ -0,0 +1,8 @@ +name: opnsense-sample +version: 0.1 +origin: opnsense/sample +comment: OPNsense configuration template example +desc: creates some files in /tmp/.../ based on reporting definitions found in +TARGETS +maintainer: ad@opnsense.org +www: https://opnsense.org +prefix: / diff --git a/src/opnsense/service/templates/OPNsense/Sample/+TARGETS b/src/opnsense/service/templates/OPNsense/Sample/+TARGETS new file mode 100644 index 000000000..997a5e802 --- /dev/null +++ b/src/opnsense/service/templates/OPNsense/Sample/+TARGETS @@ -0,0 +1,2 @@ +example_config.txt:/tmp/template_sample/test_[interfaces.%.if]_[version].conf +example_simple_page.txt:/tmp/template_sample/simple_page.txt \ No newline at end of file diff --git a/src/opnsense/service/templates/OPNsense/Sample/example_config.txt b/src/opnsense/service/templates/OPNsense/Sample/example_config.txt new file mode 100644 index 000000000..46488444f --- /dev/null +++ b/src/opnsense/service/templates/OPNsense/Sample/example_config.txt @@ -0,0 +1,19 @@ +{% extends "/OPNsense/Sample/example_parent.txt" %} +{% block content %} + +For demonstration purposes this template is based upon another template in the OPNsense/Sample module. +The block named "content" is replaced with this data + +In the +TARGETS file, the name of the output is describes as elements of the config data, because we will receive +all of the config.xml data when rendering this template, we have to filter the parts we need. + +For example, only use this interfaces data: + +{% for key,item in interfaces.iteritems() %} + {% if TARGET_FILTERS['interfaces.'+key] %} + my interface name is {{ key }}, connected to {{ item.if }} using {{ item.ipaddr }} + {% endif %} +{% endfor %} + +{% endblock %} + diff --git a/src/opnsense/service/templates/OPNsense/Sample/example_parent.txt b/src/opnsense/service/templates/OPNsense/Sample/example_parent.txt new file mode 100644 index 000000000..344a736e3 --- /dev/null +++ b/src/opnsense/service/templates/OPNsense/Sample/example_parent.txt @@ -0,0 +1,2 @@ +the parent template +{% block content %}{% endblock %} diff --git a/src/opnsense/service/templates/OPNsense/Sample/example_simple_page.txt b/src/opnsense/service/templates/OPNsense/Sample/example_simple_page.txt new file mode 100644 index 000000000..adeaba81f --- /dev/null +++ b/src/opnsense/service/templates/OPNsense/Sample/example_simple_page.txt @@ -0,0 +1,34 @@ +This is a very simple example of how to generate configuration files based on templates. + +In +TARGETS you can find the mapping between this template and the output it should generate. + +Now lets retrieve some configuration data from the OPNsense configuration file: + + +last change date : {{ lastchange|default('unknown') }} +version according to config.xml : {{ version|default('?') }} + +list of configured network interfaces : + +{% for key,item in interfaces.iteritems() %} + interface {{ key }} + --- interface {{ item.if }} + --- address {{ item.ipaddr }} + --- subnet {{ item.subnet }} + +{% endfor %} + +and a short list of firewall rules created (multiple rule items in filter section): + +{% for item in filter.rule%} + descr : {{ item.descr }} + type : {{ item.type }} + interface : {{ item.interface }} ( which has ip {{ interfaces[item.interface].ipaddr }} ) + +{% endfor %} + +The full documentation for the template engine can be found at : http://jinja.pocoo.org/docs/dev/templates/ + +A sample with multiple output files ( for example based on interface ) can be found in the example_config.txt template + +