From 086857baa69d49006bbdddc93f8bf9c94297c553 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Thu, 25 May 2017 18:30:57 +0200 Subject: [PATCH] [general] Refactored backends, renderers, added converters #70 This is the first step in implementing #70. This patch does not introduces changes of behaviour in the library, infact tests remain unaltered, except for their names (because they were named after renderer classes, which now are a much smaller part of the library). This patch introduces the concept of converter classes, which are responsible for converting the NetJSON configuration dictionary to an intermediate data structure that is then used by renderers to be rendered. --- .../{openvpn/templates => base}/__init__.py | 0 .../backends/{base.py => base/backend.py} | 111 ++--- netjsonconfig/backends/base/converter.py | 40 ++ netjsonconfig/backends/base/renderer.py | 47 ++ .../openvpn/{renderers.py => converters.py} | 31 +- netjsonconfig/backends/openvpn/openvpn.py | 15 +- netjsonconfig/backends/openvpn/renderer.py | 14 + .../backends/openvpn/templates/openvpn.jinja2 | 2 +- netjsonconfig/backends/openwisp/openwisp.py | 20 +- .../openwrt/{renderers.py => converters.py} | 452 +++++++++--------- netjsonconfig/backends/openwrt/openwrt.py | 44 +- netjsonconfig/backends/openwrt/renderer.py | 25 + .../backends/openwrt/templates/default.jinja2 | 22 - .../backends/openwrt/templates/network.jinja2 | 64 --- .../backends/openwrt/templates/openvpn.jinja2 | 18 - .../backends/openwrt/templates/openwrt.jinja2 | 20 + .../backends/openwrt/templates/system.jinja2 | 37 -- .../openwrt/templates/wireless.jinja2 | 34 -- netjsonconfig/exceptions.py | 10 +- runflake8 | 7 +- tests/openwrt/test_backend.py | 5 +- tests/openwrt/test_context.py | 2 + tests/openwrt/test_default.py | 7 +- tests/openwrt/test_encryption.py | 3 - tests/openwrt/test_network.py | 32 +- tests/openwrt/test_openvpn.py | 5 +- tests/openwrt/test_parse.py | 240 ++++++++++ tests/openwrt/test_radio.py | 3 - tests/openwrt/test_system.py | 5 +- tests/openwrt/test_wireless.py | 3 - tests/test_base.py | 3 +- 31 files changed, 752 insertions(+), 569 deletions(-) rename netjsonconfig/backends/{openvpn/templates => base}/__init__.py (100%) rename netjsonconfig/backends/{base.py => base/backend.py} (73%) create mode 100644 netjsonconfig/backends/base/converter.py create mode 100644 netjsonconfig/backends/base/renderer.py rename netjsonconfig/backends/openvpn/{renderers.py => converters.py} (62%) create mode 100644 netjsonconfig/backends/openvpn/renderer.py rename netjsonconfig/backends/openwrt/{renderers.py => converters.py} (64%) create mode 100644 netjsonconfig/backends/openwrt/renderer.py delete mode 100644 netjsonconfig/backends/openwrt/templates/default.jinja2 delete mode 100644 netjsonconfig/backends/openwrt/templates/network.jinja2 delete mode 100644 netjsonconfig/backends/openwrt/templates/openvpn.jinja2 create mode 100644 netjsonconfig/backends/openwrt/templates/openwrt.jinja2 delete mode 100644 netjsonconfig/backends/openwrt/templates/system.jinja2 delete mode 100644 netjsonconfig/backends/openwrt/templates/wireless.jinja2 create mode 100644 tests/openwrt/test_parse.py diff --git a/netjsonconfig/backends/openvpn/templates/__init__.py b/netjsonconfig/backends/base/__init__.py similarity index 100% rename from netjsonconfig/backends/openvpn/templates/__init__.py rename to netjsonconfig/backends/base/__init__.py diff --git a/netjsonconfig/backends/base.py b/netjsonconfig/backends/base/backend.py similarity index 73% rename from netjsonconfig/backends/base.py rename to netjsonconfig/backends/base/backend.py index d2a82bf67..d95bb4c87 100644 --- a/netjsonconfig/backends/base.py +++ b/netjsonconfig/backends/base/backend.py @@ -1,17 +1,17 @@ import gzip import json import tarfile +from collections import OrderedDict from copy import deepcopy from io import BytesIO import six -from jinja2 import Environment, PackageLoader from jsonschema import FormatChecker, validate from jsonschema.exceptions import ValidationError as JsonSchemaError -from ..exceptions import ValidationError -from ..schema import DEFAULT_FILE_MODE -from ..utils import evaluate_vars, merge_config +from ...exceptions import ValidationError +from ...schema import DEFAULT_FILE_MODE +from ...utils import evaluate_vars, merge_config class BaseBackend(object): @@ -19,8 +19,8 @@ class BaseBackend(object): Base Backend class """ schema = None - env_path = 'netjsonconfig.backends.base' FILE_SECTION_DELIMITER = '# ---------- files ---------- #' + intermediate_data = None def __init__(self, config, templates=[], context={}): """ @@ -35,8 +35,6 @@ def __init__(self, config, templates=[], context={}): config = deepcopy(self._load(config)) self.config = self._merge_config(config, templates) self.config = self._evaluate_vars(self.config, context) - self.env = Environment(loader=PackageLoader(self.env_path, 'templates'), - trim_blocks=True) def _load(self, config): """ @@ -113,29 +111,23 @@ def render(self, files=True): :returns: string with output """ self.validate() - output = '' - # iterate over the available renderers - # to build the target configuration - # from the configuration dictionary - for renderer_class in self.renderers: - renderer = renderer_class(self) - additional_output = renderer.render() - # add an additional new line - # to separate blocks of configuration - # generated by different renderers - if output and additional_output: - output += '\n' - # concatenate the render configuration - output += additional_output - + # convert NetJSON config to intermediate data structure + if self.intermediate_data is None: + self.to_intermediate() + # render intermediate data structure into native configuration + renderer = self.renderer(self) + output = renderer.render() + # remove reference to renderer instance (not needed anymore) + del renderer # are we required to include # additional files? if files: # render additional files files_output = self._render_files() if files_output: - output += files_output.replace('\n\n\n', '\n\n') # max 3 \n - # finally return the whole configuration + # max 2 new lines + output += files_output.replace('\n\n\n', '\n\n') + # return the configuration return output def json(self, validate=True, *args, **kwargs): @@ -235,59 +227,22 @@ def _add_file(self, tar, name, contents, mode=DEFAULT_FILE_MODE): info.mode = int(mode, 8) # permissions converted to decimal notation tar.addfile(tarinfo=info, fileobj=byte_contents) - -class BaseRenderer(object): - """ - Renderers are used to generate specific configuration blocks. - """ - block_name = None - - def __init__(self, backend): - self.config = backend.config - self.env = backend.env - self.backend = backend - - @classmethod - def get_name(cls): + def to_intermediate(self): """ - Get the name of the rendered without prefix + Converts the NetJSON configuration dictionary (self.config) + to the intermediate data structure (self.intermediate_data) that will + be then used by the renderer class to generate the router configuration """ - return str(cls.__name__).replace('Renderer', '').lower() - - def cleanup(self, output): - """ - Performs cleanup of output (indentation, new lines) - - :param output: string representation of the client configuration - """ - return output - - def render(self): - """ - Renders config block with jinja2 templating engine - """ - # get jinja2 template - template_name = '{0}.jinja2'.format(self.get_name()) - template = self.env.get_template(template_name) - # render template and cleanup - context = self.get_context() - output = template.render(**context) - return self.cleanup(output) - - def get_context(self): - """ - Builds context dictionary to be used in jinja2 templates - For every method prefixed with `__get`, such as ``_get_name``, creates an entry in - the context dictionary whose key is ``name`` and it's value - it the output of ``_get_name`` - """ - # get list of private methods that start with "_get_" - methods = [method for method in dir(self) if method.startswith('_get_')] - context = {} - # build context - for method in methods: - key = method.replace('_get_', '') - context[key] = getattr(self, method)() - # determine if all context values are empty - context['is_empty'] = not any(context.values()) - return context + self.validate() + data = OrderedDict() + for converter_class in self.converters: + # skip unnecessary loop cycles + if not converter_class.should_run(self.config): + continue + converter = converter_class(self) + for item in converter.to_intermediate(): + key, value = item + if value: + data.setdefault(key, []) + data[key] += value + self.intermediate_data = data diff --git a/netjsonconfig/backends/base/converter.py b/netjsonconfig/backends/base/converter.py new file mode 100644 index 000000000..472bf20df --- /dev/null +++ b/netjsonconfig/backends/base/converter.py @@ -0,0 +1,40 @@ +from copy import deepcopy + + +class BaseConverter(object): + """ + Base Converter class + Converters are used to convert a configuration dictionary + which represent a NetJSON object to a data structure that + can be easily rendered as the final router configuration + and vice versa. + """ + netjson_key = None + + def __init__(self, backend): + self.backend = backend + self.netjson = backend.config + self.intermediate_data = backend.intermediate_data + + @classmethod + def should_run(cls, config): + """ + Returns True if Converter should be instantiated and run + Used to skip processing if the configuration part related to + the converter is not present in the configuration dictionary. + """ + netjson_key = cls.netjson_key or cls.__name__.lower() + return netjson_key in config + + def get_copy(self, dict_, key, default=None): + """ + Looks for a key in a dictionary, if found returns + a deepcopied value, otherwise returns default value + """ + value = dict_.get(key, default) + if value: + return deepcopy(value) + return value + + def to_intermediate(self): + raise NotImplementedError() diff --git a/netjsonconfig/backends/base/renderer.py b/netjsonconfig/backends/base/renderer.py new file mode 100644 index 000000000..f04293d71 --- /dev/null +++ b/netjsonconfig/backends/base/renderer.py @@ -0,0 +1,47 @@ +from jinja2 import Environment, PackageLoader + + +class BaseRenderer(object): + """ + Base Renderer class + Renderers are used to generate a string + which represents the router configuration + """ + def __init__(self, backend): + self.config = backend.config + self.backend = backend + + @property + def env_path(self): + return self.__module__ + + @property + def template_env(self): + return Environment(loader=PackageLoader(self.env_path, 'templates'), trim_blocks=True) + + @classmethod + def get_name(cls): + """ + Returns the name of the render class without its prefix + """ + return str(cls.__name__).replace('Renderer', '').lower() + + def cleanup(self, output): + """ + Performs cleanup of output (indentation, new lines) + + :param output: string representation of the client configuration + """ + return output + + def render(self): + """ + Renders configuration by using the jinja2 templating engine + """ + # get jinja2 template + template_name = '{0}.jinja2'.format(self.get_name()) + template = self.template_env.get_template(template_name) + # render template and cleanup + context = getattr(self.backend, 'intermediate_data', {}) + output = template.render(data=context) + return self.cleanup(output) diff --git a/netjsonconfig/backends/openvpn/renderers.py b/netjsonconfig/backends/openvpn/converters.py similarity index 62% rename from netjsonconfig/backends/openvpn/renderers.py rename to netjsonconfig/backends/openvpn/converters.py index b8593bddc..8eee9219b 100644 --- a/netjsonconfig/backends/openvpn/renderers.py +++ b/netjsonconfig/backends/openvpn/converters.py @@ -1,23 +1,15 @@ -from copy import deepcopy - from ...utils import sorted_dict -from ..base import BaseRenderer +from ..base.converter import BaseConverter -class OpenVpnRenderer(BaseRenderer): - """ - Produces an OpenVPN configuration string - """ - def cleanup(self, output): - # remove indentations - output = output.replace(' ', '') - # remove last newline - if output.endswith('\n\n'): - output = output[0:-1] - return output +class OpenVpn(BaseConverter): + def to_intermediate(self): + result = [] + for vpn in self.get_copy(self.netjson, 'openvpn'): + result.append(sorted_dict(self.__get_vpn(vpn))) + return (('openvpn', result),) - def _transform_vpn(self, vpn): - config = deepcopy(vpn) + def __get_vpn(self, config): skip_keys = ['script_security', 'remote'] delete_keys = [] # allow server_bridge to be empty and still rendered @@ -41,10 +33,3 @@ def _transform_vpn(self, vpn): if 'status' not in config and 'status_version' in config: del config['status_version'] return config - - def _get_openvpn(self): - openvpn = [] - for vpn in self.config.get('openvpn', []): - config = self._transform_vpn(vpn) - openvpn.append(sorted_dict(config)) - return openvpn diff --git a/netjsonconfig/backends/openvpn/openvpn.py b/netjsonconfig/backends/openvpn/openvpn.py index 29b582e2e..d3206a16c 100644 --- a/netjsonconfig/backends/openvpn/openvpn.py +++ b/netjsonconfig/backends/openvpn/openvpn.py @@ -1,17 +1,20 @@ import re -from ..base import BaseBackend -from .renderers import OpenVpnRenderer +from . import converters +from ..base.backend import BaseBackend +from .parser import OpenVpnParser +from .renderer import OpenVpnRenderer from .schema import schema class OpenVpn(BaseBackend): """ - OpenVPN 2.3 backend + OpenVPN 2.x Configuration Backend """ schema = schema - env_path = 'netjsonconfig.backends.openvpn' - renderers = [OpenVpnRenderer] + converters = [converters.OpenVpn] + parser = OpenVpnParser + renderer = OpenVpnRenderer VPN_REGEXP = re.compile('# openvpn config: ') def _generate_contents(self, tar): @@ -26,7 +29,7 @@ def _generate_contents(self, tar): vpn_instances = self.VPN_REGEXP.split(text) if '' in vpn_instances: vpn_instances.remove('') - # for each package create a file with its contents in /etc/config + # create a file for each VPN for vpn in vpn_instances: lines = vpn.split('\n') vpn_name = lines[0] diff --git a/netjsonconfig/backends/openvpn/renderer.py b/netjsonconfig/backends/openvpn/renderer.py new file mode 100644 index 000000000..e9414a939 --- /dev/null +++ b/netjsonconfig/backends/openvpn/renderer.py @@ -0,0 +1,14 @@ +from ..base.renderer import BaseRenderer + + +class OpenVpnRenderer(BaseRenderer): + """ + OpenVPN Renderer + """ + def cleanup(self, output): + # remove indentations + output = output.replace(' ', '') + # remove last newline + if output.endswith('\n\n'): + output = output[0:-1] + return output diff --git a/netjsonconfig/backends/openvpn/templates/openvpn.jinja2 b/netjsonconfig/backends/openvpn/templates/openvpn.jinja2 index e96cd1e85..cd1e28c4e 100644 --- a/netjsonconfig/backends/openvpn/templates/openvpn.jinja2 +++ b/netjsonconfig/backends/openvpn/templates/openvpn.jinja2 @@ -1,4 +1,4 @@ -{% for vpn in openvpn %} +{% for vpn in data.openvpn %} # openvpn config: {{ vpn.pop('name') }} {% for key, value in vpn.items() %} diff --git a/netjsonconfig/backends/openwisp/openwisp.py b/netjsonconfig/backends/openwisp/openwisp.py index 0da9323ed..930f3106b 100644 --- a/netjsonconfig/backends/openwisp/openwisp.py +++ b/netjsonconfig/backends/openwisp/openwisp.py @@ -7,10 +7,10 @@ class OpenWisp(OpenWrt): - """ OpenWisp 1.x Backend """ + """ + OpenWISP 1.x Firmware (legacy) Configuration Backend + """ schema = schema - openwisp_env = Environment(loader=PackageLoader('netjsonconfig.backends.openwisp', 'templates'), - trim_blocks=True) def validate(self): self._sanitize_radios() @@ -26,7 +26,9 @@ def _sanitize_radios(self): radio.setdefault('disabled', False) def _render_template(self, template, context={}): - template = self.openwisp_env.get_template(template) + openwisp_env = Environment(loader=PackageLoader(self.__module__, 'templates'), + trim_blocks=True) + template = openwisp_env.get_template(template) return template.render(**context) def _add_unique_file(self, item): @@ -140,14 +142,6 @@ def _add_tc_script(self): "mode": "755" }) - def generate(self): - """ - Generates an openwisp configuration archive. - - :returns: in-memory tar.gz archive, instance of ``BytesIO`` - """ - return super(OpenWisp, self).generate() - def _generate_contents(self, tar): """ Adds configuration files to tarfile instance. @@ -160,7 +154,7 @@ def _generate_contents(self, tar): packages = re.split('package ', uci) if '' in packages: packages.remove('') - # for each package create a file with its contents in /etc/config + # create a file for each configuration package used for package in packages: lines = package.split('\n') package_name = lines[0] diff --git a/netjsonconfig/backends/openwrt/renderers.py b/netjsonconfig/backends/openwrt/converters.py similarity index 64% rename from netjsonconfig/backends/openwrt/renderers.py rename to netjsonconfig/backends/openwrt/converters.py index 453b8aeb6..171994fef 100644 --- a/netjsonconfig/backends/openwrt/renderers.py +++ b/netjsonconfig/backends/openwrt/converters.py @@ -3,61 +3,87 @@ from ipaddress import ip_interface, ip_network from ...utils import sorted_dict -from ..base import BaseRenderer -from ..openvpn.renderers import OpenVpnRenderer as BaseOpenVpnRenderer +from ..base.converter import BaseConverter +from ..openvpn.converters import OpenVpn as BaseOpenVpn from .schema import default_radio_driver -from .timezones import timezones +from .timezones import timezones, timezones_reversed def logical_name(name): return name.replace('.', '_').replace('-', '_') -class BaseOpenWrtRenderer(BaseRenderer): - """ - Base OpenWrt Renderer - """ - def cleanup(self, output): - """ - OpenWRT specific output cleanup - """ - # correct indentation - output = output.replace(' ', '')\ - .replace('\noption', '\n\toption')\ - .replace('\nlist', '\n\tlist') - # convert True to 1 and False to 0 - output = output.replace('True', '1')\ - .replace('False', '0') - # max 2 consecutive \n delimiters - output = output.replace('\n\n\n', '\n\n') - # if output is present - # ensure it always ends with 1 new line - if output.endswith('\n\n'): - return output[0:-1] - return output - - -class NetworkRenderer(BaseOpenWrtRenderer): - """ - Renders content importable with: - uci import network - """ - def _get_interfaces(self): - """ - converts interfaces object to UCI interface directives - """ - interfaces = self.config.get('interfaces', []) +class General(BaseConverter): + def to_intermediate(self): + general = self.get_copy(self.netjson, 'general') + network = self.__get_ula(general) + system = self.__get_system(general) + return ( + ('system', system), + ('network', network) + ) + + def __get_system(self, general): + if not general: + return None + timezone_human = general.get('timezone', 'UTC') + timezone_value = timezones[timezone_human] + general.update({ + '.type': 'system', + '.name': 'system', + 'hostname': general.get('hostname', 'OpenWRT'), + 'timezone': timezone_value, + 'zonename': timezone_human, + }) + return [sorted_dict(general)] + + def __get_ula(self, general): + if 'ula_prefix' in general: + ula = { + '.type': 'globals', + '.name': 'globals', + 'ula_prefix': general.pop('ula_prefix') + } + return [sorted_dict(ula)] + return None + + +class Ntp(BaseConverter): + def to_intermediate(self): + ntp = self.get_copy(self.netjson, 'ntp') + result = None + if ntp: + ntp.update({ + '.type': 'timeserver', + '.name': 'ntp', + }) + result = [sorted_dict(ntp)] + return (('system', result),) + + +class Led(BaseConverter): + def to_intermediate(self): + result = [] + for led in self.get_copy(self.netjson, 'led'): + led.update({ + '.type': 'led', + '.name': 'led_{0}'.format(led['name'].lower()), + }) + result.append(sorted_dict(led)) + return (('system', result),) + + +class Interfaces(BaseConverter): + def to_intermediate(self): + result = [] # this line ensures interfaces are not entirely # ignored if they do not contain any address default_address = [{'proto': 'none'}] - # results container - uci_interfaces = [] - for interface in interfaces: - counter = 1 + for interface in self.get_copy(self.netjson, 'interfaces'): + i = 1 is_bridge = False # determine uci logical interface name - network = interface.get('network') - uci_name = interface['name'] if not network else network + uci_name = interface.get('network', interface['name']) # convert dot and dashes to underscore uci_name = logical_name(uci_name) # determine if must be type bridge @@ -72,7 +98,7 @@ def _get_interfaces(self): for address in address_list: # prepare new UCI interface directive uci_interface = deepcopy(interface) - if network: + if 'network' in uci_interface: del uci_interface['network'] if 'mac' in uci_interface: if interface.get('type') != 'wireless': @@ -96,8 +122,8 @@ def _get_interfaces(self): netmask = None proto = self.__get_proto(uci_interface, address) # add suffix if there is more than one config block - if counter > 1: - name = '{name}_{counter}'.format(name=uci_name, counter=counter) + if i > 1: + name = '{name}_{i}'.format(name=uci_name, i=i) else: name = uci_name if address.get('family') == 'ipv4': @@ -114,8 +140,9 @@ def _get_interfaces(self): address_value = address['address'] # update interface dict uci_interface.update({ - 'name': name, - 'ifname': interface['name'], + '.name': name, + '.type': 'interface', + 'ifname': uci_interface.pop('name'), 'proto': proto, 'dns': self.__get_dns_servers(uci_interface, address), 'dns_search': self.__get_dns_search(uci_interface, address) @@ -155,9 +182,9 @@ def _get_interfaces(self): del address_copy[key] uci_interface.update(address_copy) # append to interface list - uci_interfaces.append(sorted_dict(uci_interface)) - counter += 1 - return uci_interfaces + result.append(sorted_dict(uci_interface)) + i += 1 + return (('network', result),) def __get_proto(self, interface, address): """ @@ -170,55 +197,6 @@ def __get_proto(self, interface, address): # allow override return interface['proto'] - def _get_routes(self): - routes = self.config.get('routes', []) - # results container - uci_routes = [] - counter = 1 - # build uci_routes - for route in routes: - # prepare UCI route directive - uci_route = route.copy() - del uci_route['device'] - del uci_route['next'] - del uci_route['destination'] - del uci_route['cost'] - network = ip_interface(route['destination']) - version = 'route' if network.version == 4 else 'route6' - target = network.ip if network.version == 4 else network.network - uci_route.update({ - 'version': version, - 'name': 'route{0}'.format(counter), - 'interface': route['device'], - 'target': str(target), - 'gateway': route['next'], - 'metric': route['cost'], - 'source': route.get('source') - }) - if network.version == 4: - uci_route['netmask'] = str(network.netmask) - uci_routes.append(sorted_dict(uci_route)) - counter += 1 - return uci_routes - - def _get_ip_rules(self): - rules = self.config.get('ip_rules', []) - uci_rules = [] - for rule in rules: - uci_rule = rule.copy() - src_net = None - dest_net = None - family = 4 - if rule.get('src'): - src_net = ip_network(rule['src']) - if rule.get('dest'): - dest_net = ip_network(rule['dest']) - if dest_net or src_net: - family = dest_net.version if dest_net else src_net.version - uci_rule['block_name'] = 'rule{0}'.format(family).replace('4', '') - uci_rules.append(sorted_dict(uci_rule)) - return uci_rules - def __get_dns_servers(self, uci, address): # allow override if 'dns' in uci: @@ -227,7 +205,7 @@ def __get_dns_servers(self, uci, address): if address['proto'] in ['dhcp', 'none']: return None # general setting - dns = self.config.get('dns_servers', None) + dns = self.netjson.get('dns_servers', None) if dns: return ' '.join(dns) @@ -239,87 +217,119 @@ def __get_dns_search(self, uci, address): if address['proto'] == 'none': return None # general setting - dns_search = self.config.get('dns_search', None) + dns_search = self.netjson.get('dns_search', None) if dns_search: return ' '.join(dns_search) - def _get_switches(self): - uci_switches = [] - for switch in self.config.get('switch', []): - uci_switch = sorted_dict(deepcopy(switch)) - uci_switch['vlan'] = [sorted_dict(vlan) for vlan in uci_switch['vlan']] - uci_switches.append(uci_switch) - return uci_switches - - def _get_globals(self): - ula_prefix = self.config.get('general', {}).get('ula_prefix', None) - if ula_prefix: - return {'ula_prefix': ula_prefix} - return {} - - -class SystemRenderer(BaseOpenWrtRenderer): - """ - Renders content importable with: - uci import system - """ - def _get_system(self): - general = self.config.get('general', {}).copy() - # ula_prefix is not related to system - if 'ula_prefix' in general: - del general['ula_prefix'] - if general: - timezone_human = general.get('timezone', 'UTC') - timezone_value = timezones[timezone_human] - general.update({ - 'hostname': general.get('hostname', 'OpenWRT'), - 'timezone': timezone_value, - 'zonename': timezone_human, + +class Routes(BaseConverter): + __delete_keys = ['device', 'next', 'destination', 'cost'] + + def to_intermediate(self): + result = [] + i = 1 + for route in self.get_copy(self.netjson, 'routes'): + network = ip_interface(route['destination']) + target = network.ip if network.version == 4 else network.network + route.update({ + '.type': 'route{0}'.format('6' if network.version == 6 else ''), + '.name': 'route{0}'.format(i), + 'interface': route['device'], + 'target': str(target), + 'gateway': route['next'], + 'metric': route['cost'], + 'source': route.get('source') + }) + if network.version == 4: + route['netmask'] = str(network.netmask) + for key in self.__delete_keys: + del route[key] + result.append(sorted_dict(route)) + i += 1 + return (('network', result),) + + +class Rules(BaseConverter): + netjson_key = 'ip_rules' + + def to_intermediate(self): + result = [] + i = 1 + for rule in self.get_copy(self.netjson, 'ip_rules'): + src_net = None + dest_net = None + family = 4 + if 'src' in rule: + src_net = ip_network(rule['src']) + if 'dest' in rule: + dest_net = ip_network(rule['dest']) + if dest_net or src_net: + family = dest_net.version if dest_net else src_net.version + rule.update({ + '.type': 'rule{0}'.format(family).replace('4', ''), + '.name': 'rule{0}'.format(i), + }) + result.append(sorted_dict(rule)) + i += 1 + return (('network', result),) + + +class Switch(BaseConverter): + def to_intermediate(self): + result = [] + for switch in self.get_copy(self.netjson, 'switch'): + switch.update({ + '.type': 'switch', + '.name': switch['name'], + }) + i = 1 + vlans = [] + for vlan in switch['vlan']: + vlan.update({ + '.type': 'switch_vlan', + '.name': '{0}_vlan{1}'.format(switch['name'], i) + }) + vlans.append(sorted_dict(vlan)) + i += 1 + del switch['vlan'] + result.append(sorted_dict(switch)) + result += vlans + return (('network', result),) + + +class Radios(BaseConverter): + _delete_keys = ['name', 'protocol', 'channel_width'] + + def to_intermediate(self): + result = [] + for radio in self.get_copy(self.netjson, 'radios'): + radio.update({ + '.type': 'wifi-device', + '.name': radio['name'], }) - return sorted_dict(general) - - def _get_ntp(self): - return sorted_dict(self.config.get('ntp', {})) - - def _get_leds(self): - uci_leds = [] - for led in self.config.get('led', []): - uci_leds.append(sorted_dict(led)) - return uci_leds - - -class WirelessRenderer(BaseOpenWrtRenderer): - """ - Renders content importable with: - uci import wireless - """ - def _get_radios(self): - radios = self.config.get('radios', []) - uci_radios = [] - for radio in radios: - uci_radio = radio.copy() # rename tx_power to txpower if 'tx_power' in radio: - uci_radio['txpower'] = radio['tx_power'] - del uci_radio['tx_power'] + radio['txpower'] = radio['tx_power'] + del radio['tx_power'] # rename driver to type - uci_radio['type'] = uci_radio.pop('driver', default_radio_driver) + radio['type'] = radio.pop('driver', default_radio_driver) # determine hwmode option - uci_radio['hwmode'] = self.__get_hwmode(radio) - del uci_radio['protocol'] + radio['hwmode'] = self.__get_hwmode(radio) # check if using channel 0, that means "auto" - if uci_radio['channel'] is 0: - uci_radio['channel'] = 'auto' + if radio['channel'] is 0: + radio['channel'] = 'auto' # determine channel width - if uci_radio['type'] == 'mac80211': - uci_radio['htmode'] = self.__get_htmode(radio) - del uci_radio['channel_width'] + if radio['type'] == 'mac80211': + radio['htmode'] = self.__get_htmode(radio) # ensure country is uppercase - if uci_radio.get('country'): - uci_radio['country'] = uci_radio['country'].upper() + if radio.get('country'): + radio['country'] = radio['country'].upper() + # delete unneded keys + for key in self._delete_keys: + del radio[key] # append sorted dict - uci_radios.append(sorted_dict(uci_radio)) - return uci_radios + result.append(sorted_dict(radio)) + return (('wireless', result),) def __get_hwmode(self, radio): """ @@ -353,28 +363,32 @@ def __get_htmode(self, radio): # disables n return 'NONE' - def _get_wifi_interfaces(self): - # select interfaces that have type == "wireless" - wifi_interfaces = [i for i in self.config.get('interfaces', []) - if 'wireless' in i] - # results container - uci_wifi_ifaces = [] - for wifi_interface in wifi_interfaces: - wireless = wifi_interface['wireless'] + +class Wireless(BaseConverter): + netjson_key = 'interfaces' + + def to_intermediate(self): + result = [] + for interface in self.get_copy(self.netjson, 'interfaces'): + if 'wireless' not in interface: + continue + wireless = interface['wireless'] # prepare UCI wifi-iface directive uci_wifi = wireless.copy() # inherit "disabled" attribute if present - uci_wifi['disabled'] = wifi_interface.get('disabled') + uci_wifi['disabled'] = interface.get('disabled') # add ifname - uci_wifi['ifname'] = wifi_interface['name'] - # uci identifier - uci_wifi['id'] = 'wifi_{0}'.format(logical_name(wifi_interface['name'])) + uci_wifi['ifname'] = interface['name'] + uci_wifi.update({ + '.name': 'wifi_{0}'.format(logical_name(interface['name'])), + '.type': 'wifi-iface', + }) # rename radio to device uci_wifi['device'] = wireless['radio'] del uci_wifi['radio'] # mac address override - if 'mac' in wifi_interface: - uci_wifi['macaddr'] = wifi_interface['mac'] + if 'mac' in interface: + uci_wifi['macaddr'] = interface['mac'] # map netjson wifi modes to uci wifi modes modes = { 'access_point': 'ap', @@ -408,13 +422,13 @@ def _get_wifi_interfaces(self): # but this behaviour can be overridden if not uci_wifi.get('network'): # get network, default to ifname - network = wifi_interface.get('network', wifi_interface['name']) + network = interface.get('network', interface['name']) uci_wifi['network'] = [network] uci_wifi['network'] = ' '.join(uci_wifi['network'])\ .replace('.', '_')\ .replace('-', '_') - uci_wifi_ifaces.append(sorted_dict(uci_wifi)) - return uci_wifi_ifaces + result.append(sorted_dict(uci_wifi)) + return (('wireless', result),) def __get_encryption(self, wireless): encryption = wireless.get('encryption', {}) @@ -457,22 +471,39 @@ def __get_encryption(self, wireless): return uci -class DefaultRenderer(BaseOpenWrtRenderer): - """ - Default OpenWrt Renderer - Allows great flexibility in defining UCI configuration in JSON format - """ - def _get_custom_packages(self): +class OpenVpn(BaseOpenVpn): + def __get_vpn(self, vpn): + config = super(OpenVpn, self).__get_vpn(vpn) + if 'disabled' in config: + config['enabled'] = not config['disabled'] + del config['disabled'] + # TODO: keep 'enabled' check until 0.6 and then drop it + elif 'disabled' not in config and 'enabled' not in config: + config['enabled'] = True + config.update({ + '.name': logical_name(config.pop('name')), + '.type': 'openvpn' + }) + return config + + +class Default(BaseConverter): + @classmethod + def should_run(cls, config): + """ Always runs """ + return True + + def to_intermediate(self): # determine config keys to ignore ignore_list = list(self.backend.schema['properties'].keys()) - ignore_list += self.backend.get_renderers() - # determine custom packages - custom_packages = {} - for key, value in self.config.items(): + # determine extra packages used + extra_packages = {} + for key, value in self.netjson.items(): if key not in ignore_list: block_list = [] # sort each config block if isinstance(value, list): + i = 1 for block in value[:]: # config block must be a dict # with a key named "config_name" @@ -482,25 +513,16 @@ def _get_custom_packages(self): print('Unrecognized config block was skipped:\n\n' '{0}\n\n'.format(json_block)) continue + block['.type'] = block.pop('config_name') + block['.name'] = block.pop('config_value', + '{0}_{1}'.format(block['.type'], i)) block_list.append(sorted_dict(block)) + i += 1 # if not a list just skip else: # pragma: nocover continue - custom_packages[key] = block_list - # sort custom packages - return sorted_dict(custom_packages) - - -class OpenVpnRenderer(BaseOpenWrtRenderer, BaseOpenVpnRenderer): - """ - Produces an OpenVPN configuration in UCI format for OpenWRT - """ - def _transform_vpn(self, vpn): - config = super(OpenVpnRenderer, self)._transform_vpn(vpn) - if 'disabled' in config: - config['enabled'] = not config['disabled'] - del config['disabled'] - # TODO: keep 'enabled' check until 0.6 and then drop it - elif 'disabled' not in config and 'enabled' not in config: - config['enabled'] = True - return config + extra_packages[key] = block_list + if extra_packages: + return sorted_dict(extra_packages).items() + # return empty tuple if no extra packages are used + return tuple() diff --git a/netjsonconfig/backends/openwrt/openwrt.py b/netjsonconfig/backends/openwrt/openwrt.py index d180b4fa8..00113e556 100644 --- a/netjsonconfig/backends/openwrt/openwrt.py +++ b/netjsonconfig/backends/openwrt/openwrt.py @@ -1,26 +1,30 @@ -import re - -from . import renderers -from ..base import BaseBackend +from . import converters +from ..base.backend import BaseBackend +from .parser import OpenWrtParser, config_path, packages_pattern +from .renderer import OpenWrtRenderer from .schema import schema class OpenWrt(BaseBackend): - """ OpenWrt Backend """ + """ + OpenWRT / LEDE Configuration Backend + """ schema = schema - env_path = 'netjsonconfig.backends.openwrt' - renderers = [ - renderers.SystemRenderer, - renderers.NetworkRenderer, - renderers.WirelessRenderer, - renderers.DefaultRenderer, - renderers.OpenVpnRenderer + converters = [ + converters.General, + converters.Ntp, + converters.Led, + converters.Interfaces, + converters.Routes, + converters.Rules, + converters.Switch, + converters.Radios, + converters.Wireless, + converters.OpenVpn, + converters.Default, ] - PACKAGE_EXP = re.compile('package ') - - @classmethod - def get_renderers(cls): - return [r.get_name() for r in cls.renderers] + parser = OpenWrtParser + renderer = OpenWrtRenderer def _generate_contents(self, tar): """ @@ -31,14 +35,14 @@ def _generate_contents(self, tar): """ uci = self.render(files=False) # create a list with all the packages (and remove empty entries) - packages = self.PACKAGE_EXP.split(uci) + packages = packages_pattern.split(uci) if '' in packages: packages.remove('') - # for each package create a file with its contents in /etc/config + # create an UCI file for each configuration package used for package in packages: lines = package.split('\n') package_name = lines[0] text_contents = '\n'.join(lines[2:]) self._add_file(tar=tar, - name='etc/config/{0}'.format(package_name), + name='{0}{1}'.format(config_path, package_name), contents=text_contents) diff --git a/netjsonconfig/backends/openwrt/renderer.py b/netjsonconfig/backends/openwrt/renderer.py new file mode 100644 index 000000000..e4d86faee --- /dev/null +++ b/netjsonconfig/backends/openwrt/renderer.py @@ -0,0 +1,25 @@ +from ..base.renderer import BaseRenderer + + +class OpenWrtRenderer(BaseRenderer): + """ + OpenWRT Renderer + """ + def cleanup(self, output): + """ + Generates consistent OpenWRT/LEDE UCI output + """ + # correct indentation + output = output.replace(' ', '')\ + .replace('\noption', '\n\toption')\ + .replace('\nlist', '\n\tlist') + # convert True to 1 and False to 0 + output = output.replace('True', '1')\ + .replace('False', '0') + # max 2 consecutive \n delimiters + output = output.replace('\n\n\n', '\n\n') + # if output is present + # ensure it always ends with 1 new line + if output.endswith('\n\n'): + return output[0:-1] + return output diff --git a/netjsonconfig/backends/openwrt/templates/default.jinja2 b/netjsonconfig/backends/openwrt/templates/default.jinja2 deleted file mode 100644 index cff8b963e..000000000 --- a/netjsonconfig/backends/openwrt/templates/default.jinja2 +++ /dev/null @@ -1,22 +0,0 @@ -{% if not is_empty %} - {% for package, config_blocks in custom_packages.items() %} - package {{ package }} - - {% for config in config_blocks %} - config {{ config.config_name }} '{% if config.config_value %}{{ config.config_value }}{% else %}{{ config.config_name }}_{{ loop.index }}{% endif %}' - {% for key, value in config.items() %} - {% if value not in ['', None] and not key.startswith('config_') %} - {% if value is not string and value is iterable %} - {% for list_value in value %} - list {{ key }} '{{ list_value }}' - {% endfor %} - {% else %} - option {{ key }} '{{ value }}' - {% endif %} - {% endif %} - {% endfor %} - - {% endfor %} - - {% endfor %} -{% endif %} diff --git a/netjsonconfig/backends/openwrt/templates/network.jinja2 b/netjsonconfig/backends/openwrt/templates/network.jinja2 deleted file mode 100644 index ec929df68..000000000 --- a/netjsonconfig/backends/openwrt/templates/network.jinja2 +++ /dev/null @@ -1,64 +0,0 @@ -{% if not is_empty %} - package network - - {% if globals %} - {% for key, value in globals.items() %} - config globals 'globals' - {% if value not in ['', None] %} - option {{ key }} '{{ value }}' - {% endif %} - {% endfor %} - {% endif %} - - {% for interface in interfaces %} - config interface '{{ interface.name }}' - {% for key, value in interface.items() %} - {% if key != 'name' and value not in ['', None] %} - {% if value is not string and value is iterable %} - {% for list_value in value %} - list {{ key }} '{{ list_value }}' - {% endfor %} - {% else %} - option {{ key }} '{{ value }}' - {% endif %} - {% endif %} - {% endfor %} - - {% endfor %} - {% for route in routes %} - config {{ route.pop('version') }} '{{ route.pop('name') }}' - {% for key, value in route.items() %} - {% if value not in ['', None] %} - option {{ key }} '{{ value }}' - {% endif %} - {% endfor %} - - {% endfor %} - {% for rule in ip_rules %} - config {{ rule.pop('block_name') }} 'rule{{ loop.index }}' - {% for key, value in rule.items() %} - {% if value not in ['', None] %} - option {{ key }} '{{ value }}' - {% endif %} - {% endfor %} - - {% endfor %} - {% for switch in switches %} - config switch '{{ switch.name }}' - {% for key, value in switch.items() %} - {% if key != 'vlan' and value not in ['', None] %} - option {{ key }} '{{ value }}' - {% endif %} - {% endfor %} - - {% for vlan in switch.vlan %} - config switch_vlan '{{ switch.name }}_vlan{{ loop.index }}' - {% for key, value in vlan.items() %} - {% if value not in ['', None] %} - option {{ key }} '{{ value }}' - {% endif %} - {% endfor %} - - {% endfor %} - {% endfor %} -{% endif %} diff --git a/netjsonconfig/backends/openwrt/templates/openvpn.jinja2 b/netjsonconfig/backends/openwrt/templates/openvpn.jinja2 deleted file mode 100644 index 52b42a94f..000000000 --- a/netjsonconfig/backends/openwrt/templates/openvpn.jinja2 +++ /dev/null @@ -1,18 +0,0 @@ -{% if not is_empty %} - package openvpn - - {% for vpn in openvpn %} - config openvpn '{{ vpn.pop('name').replace('-', '_') }}' - {% for key, value in vpn.items() %} - {% set key = key.replace('-', '_') %} - {% if value is not string and value is iterable %} - {% for element in value %} - list {{ key }} '{{ element }}' - {% endfor %} - {% elif value not in ['', None] %} - option {{ key }} '{{ value }}' - {% endif %} - {% endfor %} - - {% endfor %} -{% endif %} diff --git a/netjsonconfig/backends/openwrt/templates/openwrt.jinja2 b/netjsonconfig/backends/openwrt/templates/openwrt.jinja2 new file mode 100644 index 000000000..9c267c8d7 --- /dev/null +++ b/netjsonconfig/backends/openwrt/templates/openwrt.jinja2 @@ -0,0 +1,20 @@ +{% for package, config_blocks in data.items() %} + package {{ package }} + + {% for config in config_blocks %} + config {{ config['.type'] }} '{{ config['.name'] }}' + {% for key, value in config.items() %} + {% if value not in ['', None] and not key.startswith('.') %} + {% if value is not string and value is iterable %} + {% for list_value in value %} + list {{ key }} '{{ list_value }}' + {% endfor %} + {% else %} + option {{ key }} '{{ value }}' + {% endif %} + {% endif %} + {% endfor %} + + {% endfor %} + +{% endfor %} diff --git a/netjsonconfig/backends/openwrt/templates/system.jinja2 b/netjsonconfig/backends/openwrt/templates/system.jinja2 deleted file mode 100644 index e6b8c0699..000000000 --- a/netjsonconfig/backends/openwrt/templates/system.jinja2 +++ /dev/null @@ -1,37 +0,0 @@ -{% if not is_empty %} - package system - - {% if system %} - config system 'system' - {% for key, value in system.items() %} - {% if value not in ['', None] %} - option {{ key }} '{{ value }}' - {% endif %} - {% endfor %} - - {% endif %} - {% if ntp %} - config timeserver 'ntp' - {% for key, value in ntp.items() %} - {% if value not in ['', None] %} - {% if value is not string and value is iterable %} - {% for list_value in value %} - list {{ key }} '{{ list_value }}' - {% endfor %} - {% else %} - option {{ key }} '{{ value }}' - {% endif %} - {% endif %} - {% endfor %} - - {% endif %} - {% for led in leds %} - config led 'led_{{ led.name.lower() }}' - {% for key, value in led.items() %} - {% if value not in ['', None] %} - option {{ key }} '{{ value }}' - {% endif %} - {% endfor %} - - {% endfor %} -{% endif %} diff --git a/netjsonconfig/backends/openwrt/templates/wireless.jinja2 b/netjsonconfig/backends/openwrt/templates/wireless.jinja2 deleted file mode 100644 index d0cea44f9..000000000 --- a/netjsonconfig/backends/openwrt/templates/wireless.jinja2 +++ /dev/null @@ -1,34 +0,0 @@ -{% if not is_empty %} - package wireless - - {% for radio in radios %} - config wifi-device '{{ radio.pop('name') }}' - {% for key, value in radio.items() %} - {% if value not in ['', None] %} - {% if value is not string and value is iterable %} - {% for list_value in value %} - list {{ key }} '{{ list_value }}' - {% endfor %} - {% else %} - option {{ key }} '{{ value }}' - {% endif %} - {% endif %} - {% endfor %} - - {% endfor %} - {% for wifi_interface in wifi_interfaces %} - config wifi-iface '{{ wifi_interface.pop('id') }}' - {% for key, value in wifi_interface.items() %} - {% if value not in ['', None] %} - {% if value is not string and value is iterable %} - {% for list_value in value %} - list {{ key }} '{{ list_value }}' - {% endfor %} - {% else %} - option {{ key }} '{{ value }}' - {% endif %} - {% endif %} - {% endfor %} - - {% endfor %} -{% endif %} diff --git a/netjsonconfig/exceptions.py b/netjsonconfig/exceptions.py index 83bef6f09..52a355a5e 100644 --- a/netjsonconfig/exceptions.py +++ b/netjsonconfig/exceptions.py @@ -1,13 +1,15 @@ class NetJsonConfigException(Exception): - """ root netjsonconfig exception """ - + """ + Root netjsonconfig exception + """ def __str__(self): return "%s %s %s" % (self.__class__.__name__, self.message, self.details) class ValidationError(NetJsonConfigException): - """ error while validating schema """ - + """ + Error while validating schema + """ def __init__(self, e): """ preserve jsonschema exception attributes diff --git a/runflake8 b/runflake8 index 9fd4065ad..dfad52f04 100755 --- a/runflake8 +++ b/runflake8 @@ -1,9 +1,8 @@ #!/bin/bash set -e +complex="./netjsonconfig/backends/openwrt/converters.py" -flake8 --max-line-length=110 \ - --max-complexity=26 \ - ./netjsonconfig/backends/openwrt/renderers.py || exit 1 +flake8 --max-line-length=110 --max-complexity=26 "$complex" || exit 1 flake8 --max-line-length=110 \ --max-complexity=12 \ - --exclude=./docs/,./build/,./setup.py,./netjsonconfig/backends/openwrt/renderers.py || exit 1 + --exclude=./docs/,./build/,./setup.py,$complex || exit 1 diff --git a/tests/openwrt/test_backend.py b/tests/openwrt/test_backend.py index 635eb4a22..e1a7b8c71 100644 --- a/tests/openwrt/test_backend.py +++ b/tests/openwrt/test_backend.py @@ -11,9 +11,8 @@ class TestBackend(unittest.TestCase, _TabsMixin): - """ - tests for backends.openwrt.OpenWrt - """ + maxDiff = None + def test_config_copy(self): config = {'interfaces': []} o = OpenWrt(config) diff --git a/tests/openwrt/test_context.py b/tests/openwrt/test_context.py index 77683293f..567670765 100644 --- a/tests/openwrt/test_context.py +++ b/tests/openwrt/test_context.py @@ -9,6 +9,8 @@ class TestContext(unittest.TestCase, _TabsMixin): """ tests for configuration variables feature """ + maxDiff = None + def test_config(self): config = { "general": { diff --git a/tests/openwrt/test_default.py b/tests/openwrt/test_default.py index 0d93c5154..c523d8132 100644 --- a/tests/openwrt/test_default.py +++ b/tests/openwrt/test_default.py @@ -4,10 +4,7 @@ from netjsonconfig.utils import _TabsMixin -class TestDefaultRenderer(unittest.TestCase, _TabsMixin): - """ - tests for backends.openwrt.renderers.DefaultRenderer - """ +class TestDefault(unittest.TestCase, _TabsMixin): maxDiff = None def test_default(self): @@ -108,7 +105,7 @@ def test_warning(self): } ] }) - self.assertEqual(o.render(), 'package luci\n') + self.assertEqual(o.render(), '') def test_merge(self): template = { diff --git a/tests/openwrt/test_encryption.py b/tests/openwrt/test_encryption.py index cf7628795..ed9a6c51f 100644 --- a/tests/openwrt/test_encryption.py +++ b/tests/openwrt/test_encryption.py @@ -5,9 +5,6 @@ class TestEncryption(unittest.TestCase, _TabsMixin): - """ - tests for backends.openwrt.renderers.WirelessRenderer (wireless ifaces) - """ maxDiff = None def test_wpa2_personal(self): diff --git a/tests/openwrt/test_network.py b/tests/openwrt/test_network.py index 35ecc6a4a..d3f7aacc1 100644 --- a/tests/openwrt/test_network.py +++ b/tests/openwrt/test_network.py @@ -5,10 +5,7 @@ from netjsonconfig.utils import _TabsMixin -class TestNetworkRenderer(unittest.TestCase, _TabsMixin): - """ - tests for backends.openwrt.renderers.NetworkRenderer - """ +class TestNetwork(unittest.TestCase, _TabsMixin): maxDiff = None def test_loopback(self): @@ -672,6 +669,33 @@ def test_rules(self): """) self.assertEqual(o.render(), expected) + def test_rules_no_src_dest(self): + o = OpenWrt({ + "ip_rules": [ + { + "in": "eth0", + "out": "eth1", + "tos": 2, + "mark": "0x0/0x1", + "invert": True, + "lookup": "0", + "action": "blackhole" + } + ] + }) + expected = self._tabs("""package network + +config rule 'rule1' + option action 'blackhole' + option in 'eth0' + option invert '1' + option lookup '0' + option mark '0x0/0x1' + option out 'eth1' + option tos '2' +""") + self.assertEqual(o.render(), expected) + def test_switch(self): o = OpenWrt({ "switch": [ diff --git a/tests/openwrt/test_openvpn.py b/tests/openwrt/test_openvpn.py index 2dabcca9b..7e2c3fefd 100644 --- a/tests/openwrt/test_openvpn.py +++ b/tests/openwrt/test_openvpn.py @@ -4,10 +4,7 @@ from netjsonconfig.utils import _TabsMixin -class TestOpenVpnRenderer(_TabsMixin, unittest.TestCase): - """ - tests for backends.openwrt.renderers.OpenVpnRenderer - """ +class TestOpenVpn(_TabsMixin, unittest.TestCase): maxDiff = None def test_server_mode(self): diff --git a/tests/openwrt/test_parse.py b/tests/openwrt/test_parse.py new file mode 100644 index 000000000..d1acc8cf9 --- /dev/null +++ b/tests/openwrt/test_parse.py @@ -0,0 +1,240 @@ +import os +import unittest + +from netjsonconfig import OpenWrt +from netjsonconfig.exceptions import ParseError +from netjsonconfig.utils import _TabsMixin + + +class TestParse(unittest.TestCase, _TabsMixin): + maxDiff = None + + def test_to_intermediate_system(self): + o = OpenWrt({ + "general": { + "hostname": "test-system", + "timezone": "Europe/Rome", + "custom_setting": True, + "empty_setting1": None, + "empty_setting2": "" + } + }) + expected = { + "system": [{ + ".type": "system", + ".name": "system", + "hostname": "test-system", + "timezone": "CET-1CEST,M3.5.0,M10.5.0/3", + "zonename": "Europe/Rome", + "custom_setting": True, + "empty_setting1": None, + "empty_setting2": "", + }] + } + o.to_intermediate() + self.assertDictEqual(o.intermediate_data, expected) + + def test_parse_text_system(self): + o = OpenWrt({}) + native = self._tabs("""package system + +config system 'system' + option custom_setting '1' + option hostname 'test-system' + option timezone 'CET-1CEST,M3.5.0,M10.5.0/3' + option zonename 'Europe/Rome' +""") + o.parse(native) + expected = { + "system": [{ + ".type": "system", + ".name": "system", + "hostname": "test-system", + "timezone": "CET-1CEST,M3.5.0,M10.5.0/3", + "zonename": "Europe/Rome", + "custom_setting": "1" + }] + } + self.assertDictEqual(o.intermediate_data, expected) + expected = { + "general": { + "custom_setting": '1', + "hostname": "test-system", + "timezone": "Europe/Rome", + } + } + self.assertDictEqual(o.config, expected) + + def test_parse_text_comment(self): + o = OpenWrt({}) + native = self._tabs("""package system + +config system 'system' + # this is a comment + option hostname 'test-system' +""") + o.parse(native) + expected = { + "system": [ + { + ".type": "system", + ".name": "system", + "hostname": "test-system", + } + ] + } + self.assertDictEqual(o.intermediate_data, expected) + + def test_parse_text_multiple_packages(self): + o = OpenWrt({}) + native = self._tabs("""package system + +config system 'system' + option hostname 'test-system' + +package network + +config interface 'lan' + option ifname 'eth0' + option proto 'none' +""") + o.parse(native) + expected = { + "system": [ + { + ".type": "system", + ".name": "system", + "hostname": "test-system", + } + ], + "network": [ + { + ".type": "interface", + ".name": "lan", + "ifname": "eth0", + "proto": "none" + } + ] + } + self.assertDictEqual(o.intermediate_data, expected) + + def test_parse_anonymous_block(self): + o = OpenWrt({}) + native = self._tabs("""package network + +config interface + option ifname 'eth0' + option proto 'none' + +config interface 'vpn' + option ifname 'vpn' + option proto 'none' + +config interface + option ifname 'eth1' + option proto 'none' +""") + o.parse(native) + expected = { + "network": [ + { + ".type": "interface", + ".name": "interface_1", + "ifname": "eth0", + "proto": "none" + }, + { + ".type": "interface", + ".name": "vpn", + "ifname": "vpn", + "proto": "none", + }, + { + ".type": "interface", + ".name": "interface_3", + "ifname": "eth1", + "proto": "none" + } + ] + } + self.assertDictEqual(o.intermediate_data, expected) + + def test_parsed_renders_equally(self): + o = OpenWrt({}) + native = self._tabs("""package system + +config system 'system' + option custom_setting '1' + option hostname 'test-system' + option timezone 'CET-1CEST,M3.5.0,M10.5.0/3' + option zonename 'Europe/Rome' +""") + o.parse(native) + self.assertEqual(o.render(), native) + + def test_parse_inline_list(self): + o = OpenWrt({}) + native = self._tabs("""package network +config interface 'lan' + option ifname 'eth0 eth1' + option proto 'none' + option type 'bridge' +""") + o.parse(native) + expected = { + "network": [ + { + ".type": "interface", + ".name": "lan", + "ifname": "eth0 eth1", + "proto": "none", + "type": "bridge" + } + ] + } + self.assertDictEqual(o.intermediate_data, expected) + + def test_parse_tar_bytesio(self): + tar = OpenWrt({"general": {"hostname": "parse-tar-bytesio"}}).generate() + o = OpenWrt({}) + o.parse(tar) + expected = { + "system": [ + { + ".type": "system", + ".name": "system", + "hostname": "parse-tar-bytesio", + "timezone": "UTC", + "zonename": "UTC" + } + ] + } + self.assertDictEqual(o.intermediate_data, expected) + + def test_parse_tar_file(self): + o = OpenWrt({"general": {"hostname": "parse-tar-file"}}) + o.write(name='test', path='/tmp') + o = OpenWrt({}) + o.parse(open('/tmp/test.tar.gz')) + expected = { + "system": [ + { + ".type": "system", + ".name": "system", + "hostname": "parse-tar-file", + "timezone": "UTC", + "zonename": "UTC" + } + ] + } + os.remove('/tmp/test.tar.gz') + self.assertDictEqual(o.intermediate_data, expected) + + def test_parse_exception(self): + o = OpenWrt({}) + try: + o.parse(10) + except Exception as e: + self.assertIsInstance(e, ParseError) + else: + self.fail('Exception not raised') diff --git a/tests/openwrt/test_radio.py b/tests/openwrt/test_radio.py index 7e338108a..4a1dbbc89 100644 --- a/tests/openwrt/test_radio.py +++ b/tests/openwrt/test_radio.py @@ -5,9 +5,6 @@ class TestRadio(unittest.TestCase, _TabsMixin): - """ - tests for backends.openwrt.renderers.WirelessRenderer (radio devices) - """ maxDiff = None def test_radio(self): diff --git a/tests/openwrt/test_system.py b/tests/openwrt/test_system.py index 538652d1e..2c107a9a0 100644 --- a/tests/openwrt/test_system.py +++ b/tests/openwrt/test_system.py @@ -5,10 +5,7 @@ from netjsonconfig.utils import _TabsMixin -class TestSystemRenderer(unittest.TestCase, _TabsMixin): - """ - tests for backends.openwrt.renderers.SystemRenderer - """ +class TestSystem(unittest.TestCase, _TabsMixin): def test_system(self): o = OpenWrt({ "general": { diff --git a/tests/openwrt/test_wireless.py b/tests/openwrt/test_wireless.py index 67a92d5c2..56745fb92 100644 --- a/tests/openwrt/test_wireless.py +++ b/tests/openwrt/test_wireless.py @@ -6,9 +6,6 @@ class TestWireless(unittest.TestCase, _TabsMixin): - """ - tests for backends.openwrt.renderers.WirelessRenderer (wireless ifaces) - """ maxDiff = None def test_wifi_interfaces(self): diff --git a/tests/test_base.py b/tests/test_base.py index 07d3d46e5..0beec8ce2 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,6 +1,7 @@ import unittest -from netjsonconfig.backends.base import BaseBackend, BaseRenderer +from netjsonconfig.backends.base.backend import BaseBackend +from netjsonconfig.backends.base.renderer import BaseRenderer class TestBase(unittest.TestCase):