diff --git a/.travis.yml b/.travis.yml index ca0673e15..c732876ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,11 @@ language: python sudo: false cache: pip +addons: + apt: + packages: + - graphviz + python: - "3.5" - "3.4" diff --git a/bin/netjsonconfig b/bin/netjsonconfig index 317da7b54..5b09588c2 100644 --- a/bin/netjsonconfig +++ b/bin/netjsonconfig @@ -5,6 +5,7 @@ import sys import six import argparse import netjsonconfig +import traceback description = """ Converts a NetJSON DeviceConfiguration object to native router configurations. @@ -56,10 +57,10 @@ output = parser.add_argument_group('output') output.add_argument('--backend', '-b', required=True, - choices=['openwrt', 'openwisp', 'openvpn'], + choices=['openwrt', 'openwisp', 'openvpn', 'airos'], action='store', type=str, - help='Configuration backend') + help='Configuration backend: openwrt, openwisp or airos') output.add_argument('--method', '-m', required=True, @@ -169,7 +170,8 @@ method_arguments = parse_method_arguments(args.args) backends = { 'openwrt': netjsonconfig.OpenWrt, 'openwisp': netjsonconfig.OpenWisp, - 'openvpn': netjsonconfig.OpenVpn + 'openvpn': netjsonconfig.OpenVpn, + 'airos': netjsonconfig.AirOs, } backend_class = backends[args.backend] @@ -197,5 +199,8 @@ except netjsonconfig.exceptions.ValidationError as e: print(message + info) sys.exit(4) except TypeError as e: + if args.verbose: + traceback.print_exc() + print('netjsonconfig: {0}'.format(e)) sys.exit(5) diff --git a/docs/source/_github.rst b/docs/source/_github.rst deleted file mode 100644 index 3f8f505bb..000000000 --- a/docs/source/_github.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. raw:: html - -

- - -

diff --git a/docs/source/backends/airos-upgrade.rst b/docs/source/backends/airos-upgrade.rst new file mode 100644 index 000000000..d25888f16 --- /dev/null +++ b/docs/source/backends/airos-upgrade.rst @@ -0,0 +1,47 @@ +.. _airos-configuration-upgrade: + +Tools +----- + +AirOS is shipped with proprietary tools that can parse the configuration file and upgrade the antenna. + +cfgmtd +^^^^^^ + +This tool can write and read data to the memory that persist between reboots. + +ubntcfg +^^^^^^^ + +This tool can parse the configuration and creates the init scripts that configure the device + +rc scripts +^^^^^^^^^^ + +This are not commands but a collection of scripts that orchestrate the configuration process. As they are stored on the antenna they can be modified to obtain different behaviours. + +* update scripts are stored in `/usr/local/rc.d` +* module list is stored in `/etc/startup.list` + +The update process is orchestrated by the `/usr/local/rc.d/rc.do.softrestart` script. + +Process +------- + +AirOS mantains the device configuration in two files, both can be found in `/tmp`. + +* `/tmp/system.cfg` the target configuration +* `/tmp/running.cfg` the running configuration + +If we want to upgrade the device configuration with our file we can overwrite the target configuration and runt the commands `cfgmtd -w` and `/usr/local/rc.d/rc.do.softrestart save` + + +Full transcript of the update processs + +.. code-block:: bash + + cp /path/to/my/config.cfg /tmp/system.cfg + # writes the configuration to the persistent memory + cfgmtd -w /tmp/system.cfg + # initiate the configuration update + /usr/local/rc.d/rc.do.softrestart save diff --git a/docs/source/backends/airos.rst b/docs/source/backends/airos.rst new file mode 100644 index 000000000..7b67a2c7e --- /dev/null +++ b/docs/source/backends/airos.rst @@ -0,0 +1,418 @@ +============= +AirOS Backend +============= + +.. include:: ../_github.rst + +The ``AirOs`` backend allows to generate AirOS v8.3 compatible configurations. + +.. warning:: + + This backend is in experimental stage: it may have bugs and it will + receive backward incompatible updates during the first 6 months + of development (starting from September 2017). + Early feedback and contributions are very welcome and will help + to stabilize the backend faster. + +.. toctree:: + + intermediate + airos-upgrade + +Initialization +-------------- + +.. automethod:: netjsonconfig.AirOs.__init__ + +Initialization example: + +.. code-block:: python + + from netjsonconfig import AirOs + + router = AirOs({ + "general": { + "hostname": "MasterAntenna" + } + }) + +If you are unsure about the meaning of the initalization parameters, +read about the following basic concepts: + + * :ref:`configuration_dictionary` + * :ref:`template` + * :ref:`context` + +Render method +------------- + +.. automethod:: netjsonconfig.AirOs.render + +Generate method +--------------- + +.. automethod:: netjsonconfig.AirOs.generate + + +Write method +------------ + +.. automethod:: netjsonconfig.AirOs.write + + +JSON method +----------- + +.. automethod:: netjsonconfig.AirOs.json + + +Extending the backend +--------------------- + +Please see the :ref:`airos-intermediate-representation` page for extending converters and adding functionalities to this backend + +The configuration upgrade process +--------------------------------- + +Please see the :ref:`airos-configuration-upgrade` page for information about the process and tools that upgrades the configuration on the device + +Converters with defaults +------------------------ + +NetSJON does not map explicitly to various section of the AirOS device configuration. For those section we have provided default values that should work both in ``bridge`` and ``router`` mode. + +The list of "defaulted" converters follows: + +* Discovery +* Dhcpc + + * ``dhcpc.devname`` defaults to ``br0`` + +* Dyndns +* Httpd +* Igmpproxy +* Iptables + + * ``iptables.sys.mgmt.devname`` defaults to ``br0`` + +* Netconf + + * the first interface with a ``gateway`` specified is the management interface in ``bridge`` mode + * the first interface with a ``gateway`` specified is the ``wan`` interface in ``router`` mode + +* Pwdog +* Radio + + * most of the configuration for the radio interface is taken from a PowerBeam ``PBE-5AC-400`` + +* Syslog +* System +* Telnetd +* Tshaper +* Unms +* Update +* Upnpd + +General settings +---------------- + +From the ``general`` property we can configure the contact and the location for a device using the ``contact`` and ``location`` properties. + +The following snippet specify both contact and location: + +.. code-block:: json + + { + "type": "DeviceConfiguration", + "general": { + "contact": "user@example.com", + "location": "Up on the roof" + } + } + +Network interface +----------------- + +From the ``interfaces`` key we can configure the device network interfaces. + +AirOS supports the following types of interfaces + +* **network interfaces**: may be of type ``ethernet`` +* **wirelesss interfaces**: must be of type ``wireless`` +* **bridge interfaces**: must be of type ``bridge`` + +A network interface can be designed to be the management interfaces by setting the ``role`` key to ``mlan`` on the address chosen. + +As an example here is a snippet that set the vlan ``eth0.2`` to be the management interface on the address ``192.168.1.20`` + +.. code-block:: json + + { + "interfaces": [ + { + "name": "eth0.2", + "type": "ethernet", + "addresses": [ + { + "address": "192.168.1.20", + "family": "ipv4", + "role": "mlan", + "mask": 24, + "proto": "static" + } + ] + } + ] + } + +Ethernet +^^^^^^^^ + +The ``ethernet`` interface can be configured to allow auto-negotiation and flow control with the properties ``autoneg`` and ``flowcontrol`` + +As an example here is a snippet that enables both auto-negotiation and flow control + +.. code-block:: json + + { + "interfaces": [ + { + "type": "ethernet", + "name": "eth0", + "autoneg": true, + "flowcontrol": true + } + ] + } + +Role +^^^^ + +Interfaces can be assigned a ``role`` to mimic the web interfaces features. + +As an example setting the ``role`` property of an address to ``mlan`` will add the role ``mlan`` to the interface configuration and set it as the management interface. + +.. warning:: + + Not setting a management interface will lock you out from the web interface + +Here is the snippet to set the role to ``mlan`` + +.. code-block:: json + + { + "interfaces": [ + { + "type": "ethernet", + "name": "eth0", + "addresses": [ + { + "family": "ipv4", + "proto": "static", + "address": "192.168.1.1", + "role": "mlan" + } + ] + } + ] + } + + +This is the list of roles available for a device in ``bridge`` mode: + +* ``mlan`` for the management interface + +This is the list of roles available for a device in ``router`` mode: + +* ``wan`` for the wan interface +* ``lan`` for the lan interface + + +GUI +--- + +As an extension to `NetJSON `_ you can use the ``gui`` key to set the language of the interface + +The default values for this key are reported below + +.. code-block:: json + + { + "type": "DeviceConfiguration", + "gui": { + "language": "en_US", + } + } + +Netmode +------- + +AirOS v8.3 can operate in ``bridge`` and ``router`` mode (but defaults to ``bridge``) and this can be specified with the ``netmode`` property. + +.. code-block:: json + + { + "type": "DeviceConfiguration", + "netmode": "bridge" + } + +NTP servers +----------- + +This is an extension to the `NetJSON `_ specification. + +By setting the key ``ntp`` property in your input you can provide the configuration for the ntp client running on the device. + +.. code-block:: json + + { + "type": "DeviceConfiguration", + "ntp": { + "enabled": true, + "server": [ + "0.ubnt.pool.ntp.org" + ] + } + } + +For the lazy one we provide these defaults + +.. code-block:: json + + { + "type": "DeviceConfiguration", + "ntp": { + "enabled": true, + "server": [ + "0.pool.ntp.org", + "1.pool.ntp.org", + "2.pool.ntp.org", + "3.pool.ntp.org" + ] + } + } + +Radio +----- + +The following properties of a ``Radio Object`` are used during the conversion, the others have been set to safe defaults. + +* ``name`` + +Ssh +--- + +We can specify the configuration for the ssh server on the antenna using the ``sshd`` property. + +This snippet shows how to configure the ssh server with the default values. + +.. code-block:: json + + { + "type": "DeviceConfiguration", + "sshd": { + "port": 22, + "enabled": true, + "password_auth": true + } + } + +And this shows how to set the authorized ssh public keys + +.. code-block:: json + + { + "type": "DeviceConfiguration", + "sshd": { + "keys": [ + { + "type": "ssh-rsa", + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBEEhdDJIbHVHIXQQ8dzH3pfmIbZjlrcIV+YkZM//ezQtINTUbqolCXFsETVVwbCH6d8Pi1v1lCDgILbkOOivTIKUgG8/84yI4VLCH03CAd55IG7IFZe9e6ThT4/MryH8zXKGAq5rnQSW90ashZaOEH0wNTOhkZmQ/QhduJcarevH4iZPrq5eM/ClCXzkF0I/EWN89xKRrjMB09WmuYOT48n5Es08iJxwQ1gKfjk84Fy+hwMKVtOssfBGuYMBWByJwuvW5xCH3H6eVr1GhkBRrlTy6KAkc9kfAsSpkHIyeb/jAS2hr6kAh6cxapKENHxoAdJNvMEpdU11v6PMoOtIb edoput@hypnotoad", + "comment": "my shh key", + "enabled": true + } + ] + } + } + +Users +----- + +We can specify the user password as a blob divided into ``salt`` and ``hash``. + +From the antenna configuration take the user section. + +.. code-block:: ini + + users.status=enabled + users.1.status=enabled + users.1.name=ubnt + users.1.password=$1$yRo1tmtC$EcdoRX.JnD4VaEYgghgWg1 + +In the line ``users.1.password=$1$yRo1tmtC$EcdoRX.JnD4VaEYgghgWg1`` there are both the salt and the password hash in the format ``$ algorithm $ salt $ hash``, e.g in the previous block ``algorithm=1``, ``salt=yRo1tmtC`` and ``hash=EcdoRX.JnD4VaEYgghgWg1``. + +To specify the password in NetJSON use the ``user`` property. + +.. code-block:: json + + { + "type": "DeviceConfiguration", + "user": { + "name": "ubnt", + "passsword": "EcdoRX.JnD4VaEYgghgWg1", + "salt": "yRo1tmtC" + } + } + + +WPA2 +---- + +AirOS v8.3 supports both WPA2 personal (PSK+CCMP) and WPA2 enterprise (EAP+CCMP) as an authentication protocol. The only ciphers available is CCMP. + +As an antenna only has one wireless network available only the first wireless interface will be used during the generation. + +As an example here is a snippet that set the authentication protocol to WPA2 personal + +.. code-block:: json + + { + "interfaces": [ + { + "name": "wlan0", + "type": "wireless", + "wireless": { + "mode": "station", + "radio": "ath0", + "ssid": "ap-ssid-example", + "encryption": { + "protocol": "wpa2_personal", + "key": "changeme" + } + } + } + ] + } + +And another that set the authentication protocol to WPA2 enterprise + +.. code-block:: json + + { + "interfaces": [ + { + "name": "wlan0", + "type": "wireless", + "wireless": { + "mode": "station", + "radio": "ath0", + "ssid": "ap-ssid-example", + "encryption": { + "protocol": "wpa2_enterprise", + "identity": "my-identity", + "password": "changeme", + } + } + } + ] + diff --git a/docs/source/backends/intermediate.rst b/docs/source/backends/intermediate.rst new file mode 100644 index 000000000..86c681c6c --- /dev/null +++ b/docs/source/backends/intermediate.rst @@ -0,0 +1,246 @@ +.. _airos-intermediate-representation: + +Intermediate representation +--------------------------- + +The intermediate representation is the output of the a **converter**, +it is backend specific and is built as a tree structure made from python +builtins values. + +A tree is a *acyclic, directional graph* with an element called *root*. + +The root of our tree is stored in the first element of a tuple, along with +the root's direct sons as a list: + +.. code-block:: python + + tree = (root, direct_sons) + +As an example here we present the tree `('spam', ['eggs', 'snakes'])` + +.. graphviz:: + + digraph tree { + spam -> eggs; + spam -> snakes; + } + +As a son may be a carrier of a value so we store it in a dictionary instead of adding a *leaf* +with another level of recursion. + +As an example here we present the tree `('spam', [ { 'eggs': 2 }, { 'snakes' : { 'loved' : 'python' }}])`: + +.. graphviz:: + + digraph tree { + + eggs[label="{ eggs : 2 }"]; + loved[label="{ loved : python }"]; + + spam -> eggs; + spam -> snakes -> loved; + + } + +This tree could be tranlated to a configuration file for AirOS that looks like this: + +.. code-block:: ini + + spam.eggs=2 + spam.snakes.loved=python + + +So our tree representation is based on the simple assumption that a *leaf* is a dictionary +without nested values and nested values in a dictionary creates a father-son relationship. + +Instead when the configuration requires that the son values must be prefixed from a number, +e.g. `vlan.1.devname=eth0` we store a list of dictionaries. + +.. code-block:: python + + ( + 'spam', + [ + { + 'eggs' : 2, + }, + { + 'snakes' : { + 'loved' : [ + { + 'python2' : True, + }, + { + 'python3' : True, + }, + { + 'ipython' : True, + } + ], + }, + } + ] + ) + +And the resulting tree is: + +.. graphviz:: + + digraph tree { + + eggs[label="{ eggs : 2 }"]; + loved; + + python2[label="{ python2 : True }"]; + python3[label="{ python3 : True }"]; + ipython[label="{ ipython : True }"]; + + spam -> eggs; + spam -> snakes -> loved; + + loved -> {1}; + loved -> {2}; + loved -> {3}; + + 1 -> python2; + 2 -> python3; + 3 -> ipython; + + } + +And the configuration is: + +.. code-block:: ini + + spam.eggs=2 + spam.snakes.loved.1.python2=true + spam.snakes.loved.2.python3=true + spam.snakes.loved.2.ipython=true + +The process by which we can go from the intermediate representation from +the output configuration is called flattening, you can find more in the next section. + +Flattening +---------- + +To avoid at all cost a recursive logic in the template we flatten the intermediate +representation to something that has a *namespace* a *key* and a *value*. + +The objective is to go from a python :ref:`configuration_dictionary` that we get from loading a NetJSON to the AirOS configuration. + +An input :ref:`configuration_dictionary` is just a python dictionary, e.g.: + + +.. code-block:: python + + #python + { + 'interfaces' : [ + { + 'name' : 'eth0.1', + 'type' : 'ethernet', + 'comment' : 'management vlan' + 'comment' : 'management' + }, + { + 'name' : 'eth0.2', + 'type' : 'ethernet', + 'comment' : 'traffic' + } + ] + } + + +And this must be converted to an appropiate AirOS configuration which looks like this: + +.. code-block:: ini + + vlan.1.comment=management + vlan.1.devname=eth0 + vlan.1.id=1 + vlan.1.status=enabled + vlan.2.comment=management + vlan.2.devname=eth0 + vlan.2.id=2 + vlan.2.status=enabled + vlan.status=enabled + +To do this we must convert the :ref:`configuration_dictionary` into something that +resembles the target text, the output configuration. + +.. code-block:: python + + ( + # namespace + 'vlan', + #options + [ + { + # key : value + '1.devname' : 'eth0', + '1.id' : '1' + '1.status' : 'enabled', + '1.comment' : 'management' + }, + { + '2.devname' : 'eth0', + '2.id' : '2' + '2.status' : 'enabled', + '2.comment' : 'traffic' + } + ] + ) + +And to do that we get rid of the multiple indentation levels by flattening the tree structure. + +The tree associated with the previous NetJSON example is this: + +.. graphviz:: + + digraph tree { + vlan -> 1; + vlan -> 2; + devname1 [label="devname=eth0"]; + devname2 [label="devname=eth0"]; + + id1 [label="id=1"]; + id2 [label="id=2"]; + + status1 [label="status=enabled"]; + status2 [label="status=enabled"]; + + comment1 [label="comment=management"]; + comment2 [label="comment=traffic"]; + + 1 -> devname1; + 1 -> id1; + 1 -> status1; + 1 -> comment1; + 2 -> devname2; + 2 -> id2; + 2 -> status2; + 2 -> comment2; + } + +And by exploring depth first we get to read a line of the configuration at a time. + +E.g. following the blue line from the `vlan` root to the first `leaf` we have the +configuration `vlan.1.devname=eth0` + +.. graphviz:: + + digraph tree { + vlan -> 1 [color="blue"]; + devname1 [label="devname=eth0"]; + + id1 [label="id=1"]; + + status1 [label="status=enabled"]; + + comment1 [label="comment=management"]; + + 1 -> devname1 [color="blue"]; + 1 -> id1; + 1 -> status1; + 1 -> comment1; + } diff --git a/docs/source/backends/openvpn.rst b/docs/source/backends/openvpn.rst index 67fc83cab..2d529b76c 100644 --- a/docs/source/backends/openvpn.rst +++ b/docs/source/backends/openvpn.rst @@ -2,7 +2,7 @@ OpenVPN 2.3 Backend =================== -.. include:: ../_github.rst + The ``OpenVpn`` backend allows to generate OpenVPN 2.3.x compatible configurations. diff --git a/docs/source/backends/openwisp.rst b/docs/source/backends/openwisp.rst index 33ba5805a..9726ff82b 100644 --- a/docs/source/backends/openwisp.rst +++ b/docs/source/backends/openwisp.rst @@ -2,7 +2,7 @@ OpenWISP 1.x Backend ==================== -.. include:: ../_github.rst + The OpenWISP 1.x Backend is based on the OpenWRT backend, therefore it inherits all its features with some differences that are explained in this page. diff --git a/docs/source/backends/openwrt.rst b/docs/source/backends/openwrt.rst index 3ddfacbde..cb1447903 100644 --- a/docs/source/backends/openwrt.rst +++ b/docs/source/backends/openwrt.rst @@ -2,7 +2,7 @@ OpenWRT Backend =============== -.. include:: ../_github.rst + The ``OpenWrt`` backend allows to generate OpenWRT compatible configurations. diff --git a/docs/source/conf.py b/docs/source/conf.py index 5b3b97aab..461398200 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -35,6 +35,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', + 'sphinx.ext.graphviz', ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/source/general/basics.rst b/docs/source/general/basics.rst index 902102f59..f403cf3e8 100644 --- a/docs/source/general/basics.rst +++ b/docs/source/general/basics.rst @@ -2,7 +2,7 @@ Basic concepts ============== -.. include:: ../_github.rst + Before starting, let's quickly introduce the main concepts used in netjsonconfig: diff --git a/docs/source/general/commandline_utility.rst b/docs/source/general/commandline_utility.rst index 9e2a9cc08..55e07b98d 100644 --- a/docs/source/general/commandline_utility.rst +++ b/docs/source/general/commandline_utility.rst @@ -2,7 +2,7 @@ Command line utility ==================== -.. include:: ../_github.rst + netjsonconfig ships a command line utility that can be used from the interactive shell, bash scripts or other programming diff --git a/docs/source/general/contributing.rst b/docs/source/general/contributing.rst index 64d5c45c7..fa3461de8 100644 --- a/docs/source/general/contributing.rst +++ b/docs/source/general/contributing.rst @@ -2,7 +2,7 @@ Contributing ============ -.. include:: ../_github.rst + Thank you for taking the time to contribute to netjsonconfig. diff --git a/docs/source/general/goals.rst b/docs/source/general/goals.rst index b75c8caf5..39ba6b8d9 100644 --- a/docs/source/general/goals.rst +++ b/docs/source/general/goals.rst @@ -1,7 +1,7 @@ Motivations and Goals ===================== -.. include:: ../_github.rst + In this page we explain the goals of this project and the motivations that led us on this path. diff --git a/docs/source/general/running_tests.rst b/docs/source/general/running_tests.rst index 5b77e5d46..e2eca8c38 100644 --- a/docs/source/general/running_tests.rst +++ b/docs/source/general/running_tests.rst @@ -2,7 +2,7 @@ Running tests ============= -.. include:: ../_github.rst + Running the test suite is really straightforward! diff --git a/docs/source/general/setup.rst b/docs/source/general/setup.rst index a57caf594..1e046a9e5 100644 --- a/docs/source/general/setup.rst +++ b/docs/source/general/setup.rst @@ -2,7 +2,7 @@ Setup ===== -.. include:: ../_github.rst + Install stable version from pypi -------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 110b4c96b..6f82cb635 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,7 +20,7 @@ Netjsonconfig is part of the `OpenWISP project `_. .. image:: ./images/openwisp.org.svg :target: http://openwisp.org -.. include:: _github.rst + **netjsonconfig** is a python library that converts `NetJSON `_ *DeviceConfiguration* objects into real router configurations that can be installed @@ -48,6 +48,7 @@ Contents: /general/setup /general/basics + /backends/airos /backends/openwrt /backends/openwisp /backends/openvpn diff --git a/netjsonconfig/__init__.py b/netjsonconfig/__init__.py index 7e5e3c872..d116bfe73 100644 --- a/netjsonconfig/__init__.py +++ b/netjsonconfig/__init__.py @@ -3,3 +3,4 @@ from .backends.openwrt.openwrt import OpenWrt # noqa from .backends.openwisp.openwisp import OpenWisp # noqa from .backends.openvpn.openvpn import OpenVpn # noqa +from .backends.airos.airos import AirOs # noqa diff --git a/netjsonconfig/backends/airos/__init__.py b/netjsonconfig/backends/airos/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netjsonconfig/backends/airos/aaa.py b/netjsonconfig/backends/airos/aaa.py new file mode 100644 index 000000000..f85042426 --- /dev/null +++ b/netjsonconfig/backends/airos/aaa.py @@ -0,0 +1,162 @@ +from .interface import mode, protocol, psk, radio, ssid + + +def ap_none(interface): + """ + Returns the configuration for ``aaa`` + when in ``access_point`` mode without authentication + """ + return {} + + +def ap_psk(interface): + """ + Returns the configuration for ``aaa`` + when in ``access_point`` mode with psk authentication + """ + result = { + 'devname': radio(interface), + 'driver': 'madwifi', + 'ssid': ssid(interface), + 'wpa': { + '1.pairwise': 'CCMP', + 'key': [{'mgmt': 'WPA-PSK'}], + 'mode': 2, + 'psk': psk(interface), + } + } + return result + + +def ap_eap(interface): + """ + Return the configuration for ``aaa`` + when in ``access_point`` mode with eap authentication + """ + base = ap_psk(interface) + base.update({ + 'wpa': { + '1.pairwise': 'CCMP', + 'key': [{'mgmt': 'WPA-EAP'}], + 'mode': 2, + }, + }) + return base + + +def sta_none(interface): + """ + Return the configuration for ``aaa`` + when in station mode without authentication + """ + return {} + + +def sta_psk(interface): + """ + Return the configuration for ``aaa`` + when in station mode with psk authentication + """ + return { + 'wpa': { + 'psk': psk(interface), + } + } + + +def sta_eap(interface): + """ + Return the configuration for ``aaa`` + when in station mode with eap authentication + """ + return {} + + +_profile = {} + +_profile_from_mode = { + 'access_point': { + 'none': ap_none, + 'wpa2_personal': ap_psk, + 'wpa2_enterprise': ap_eap, + }, + 'station': { + 'none': sta_none, + 'wpa2_personal': sta_psk, + 'wpa2_enterprise': sta_eap, + }, +} + + +def profile_from_interface(interface): + """ + Returns the ``aaa`` configuration for interface + """ + profile = _profile.copy() + profile.update( + _profile_from_mode[mode(interface)][protocol(interface)](interface) + ) + return profile + + +_status = {} + +_status_from_mode = { + 'access_point': { + 'none': { + 'status': 'disabled', + }, + 'wpa2_personal': { + 'status': 'enabled', + }, + 'wpa2_enterprise': { + 'status': 'enabled', + }, + }, + 'station': { + 'none': { + 'status': 'disabled', + }, + 'wpa2_personal': { + 'status': 'disabled', + }, + 'wpa2_enterprise': { + 'status': 'disabled', + }, + } +} + + +def status_from_interface(interface): + """ + Returns ``aaa.status`` and ``aaa.1.status`` from interface + """ + status = _status.copy() + status.update( + _status_from_mode[mode(interface)][protocol(interface)] + ) + return status + + +def bridge_devname(wireless_interface, bridge_interface): + """ + when in ``access_point`` with authentication set also the + bridge interface name + + TODO: check if in ``netmode=router`` this happens again + """ + if mode(wireless_interface) == 'access_point' and protocol(wireless_interface) != 'none': + return { + 'br': { + 'devname': bridge_interface['name'], + } + } + else: + return {} + + +__all__ = [ + bridge_devname, + profile_from_interface, + status_from_interface, +] diff --git a/netjsonconfig/backends/airos/airos.py b/netjsonconfig/backends/airos/airos.py new file mode 100644 index 000000000..04f3340b2 --- /dev/null +++ b/netjsonconfig/backends/airos/airos.py @@ -0,0 +1,87 @@ +from collections import OrderedDict +from io import BytesIO +import six + +from ..base.backend import BaseBackend +from .converters import (Aaa, Bridge, Dhcpc, Discovery, Dyndns, Ebtables, Gui, + Httpd, Igmpproxy, Iptables, Netconf, Netmode, + Ntpclient, Pwdog, Radio, Resolv, Route, Snmp, Sshd, + Syslog, System, Telnetd, Tshaper, Unms, Update, Users, + Vlan, Wireless, Wpasupplicant) +from .intermediate import flatten, intermediate_to_list +from .renderers import AirOsRenderer +from .schema import schema + + +def to_ordered_list(value): + flattened = flatten(intermediate_to_list(value)) + return [OrderedDict(sorted(x.items())) for x in flattened if x != {}] + + +class AirOs(BaseBackend): + """ + AirOS backend + """ + # backend schema validator + schema = schema + # converters from configuration + # dictionary to intermediate representation + converters = [ + Aaa, + Bridge, + Dhcpc, + Discovery, + Dyndns, + Ebtables, + Gui, + Httpd, + Igmpproxy, + Iptables, + Netconf, + Netmode, + Ntpclient, + Pwdog, + Radio, + Resolv, + Route, + Snmp, + Sshd, + Syslog, + System, + Telnetd, + Tshaper, + Unms, + Update, + Users, + Vlan, + Wireless, + Wpasupplicant, + ] + # the environment where airos + # templates lives + env_path = 'netjsonconfig.backends.airos' + renderer = AirOsRenderer + + def to_intermediate(self): + super(AirOs, self).to_intermediate() + for k, v in self.intermediate_data.items(): + self.intermediate_data[k] = to_ordered_list(v) + + def generate(self): + """ + Returns a ``BytesIO`` instance representing the configuration file + + :returns: in-memory configuration file, instance of ``BytesIO`` + """ + fl = BytesIO() + fl.write(six.b(self.render())) + fl.seek(0) + return fl + + def write(self, name, path='./'): + byte_object = self.generate() + file_name = '{0}.cfg'.format(name) + if not path.endswith('/'): + path += '/' + with open('{0}{1}'.format(path, file_name), 'wb') as out: + out.write(byte_object.getvalue()) diff --git a/netjsonconfig/backends/airos/converters.py b/netjsonconfig/backends/airos/converters.py new file mode 100644 index 000000000..dc344c9a3 --- /dev/null +++ b/netjsonconfig/backends/airos/converters.py @@ -0,0 +1,792 @@ +from copy import deepcopy +from ipaddress import ip_interface + +import six + +from ...utils import get_copy +from ..base.converter import BaseConverter +from .aaa import bridge_devname, profile_from_interface, status_from_interface +from .ebtables import encrypted, unencrypted +from .interface import (autonegotiation, bridge, flowcontrol, mode, protocol, + radio, split_cidr, stp, vlan, wireless) +from .radio import radio_available_mode, radio_configuration +from .radius import radius_from_interface +from .schema import default_ntp_servers +from .wireless import wireless_available_mode +from .wpasupplicant import available_mode_authentication + + +def status(config, key='disabled'): + if config.get(key): + return 'disabled' + else: + return 'enabled' + + +class AirOsConverter(BaseConverter): + """ + Always run the converter from NetJSON + to native + """ + @classmethod + def should_run_forward(cls, config): + return True + + @property + def netmode(self): + return self.netjson.get('netmode', 'bridge') + + +class Aaa(AirOsConverter): + + @property + def bridge(self): + """ + Return all the bridge interfaces + """ + return bridge(get_copy(self.netjson, 'interfaces', [])) + + @property + def wireless(self): + """ + Return all the wireless interfaces + """ + return wireless(get_copy(self.netjson, 'interfaces', [])) + + def to_intermediate(self): + base = {} + result = [] + try: + wireless = self.wireless[0] + base.update(profile_from_interface(wireless)) + base.update(status_from_interface(wireless)) + base.update(radius_from_interface(wireless)) + except IndexError: + raise Exception('input is missing a wireless or bridge interface') + + try: + bridge = self.bridge[0] + base.update(bridge_devname(wireless, bridge)) + except IndexError: + pass + + result.append(status_from_interface(wireless)) + result.append([base]) + + return (('aaa', result),) + + +class Bridge(AirOsConverter): + netjson_key = 'interfaces' + + @property + def bridge(self): + return bridge(get_copy(self.netjson, self.netjson_key, [])) + + def to_intermediate(self): + result = [] + bridges = [] + for interface in self.bridge: + bridge_ports = [] + for port in interface.get('bridge_members', []): + bridge_ports.append({ + 'devname': port, + 'status': 'enabled', + }) + bridges.append({ + 'comment': interface.get('comment', ''), + 'devname': interface['name'], + 'port': bridge_ports, + 'status': status(interface), + 'stp': {'status': stp(interface)} + }) + + result.append(bridges) + result.append({ + 'status': 'enabled', + }) + return (('bridge', result),) + + +class Discovery(AirOsConverter): + + def to_intermediate(self): + result = [ + { + 'cdp': { + 'status': 'enabled', + }, + 'status': 'enabled', + }, + ] + return (('discovery', result),) + + +class Dhcpc(AirOsConverter): + + @classmethod + def should_run_forward(cls, config): + if config.get('netmode', 'bridge') == 'bridge': + return False + else: + return True + + def to_intermediate(self): + dhcp_interface = { + 'devname': 'br0', + 'fallback': '192.168.10.1', + 'fallback_netmask': '255.255.255.0', + 'status': 'enabled' + } + dchp_status = {'status': 'enabled'} + result = [ + dchp_status, + [dhcp_interface], + ] + return (('dhcpc', result),) + + +class Dyndns(AirOsConverter): + + def to_intermediate(self): + result = [{'status': 'disabled'}] + return (('dyndns', result),) + + +class Ebtables(AirOsConverter): + + @property + def vlan(self): + """ + Return all the vlan interfaces + """ + return vlan(get_copy(self.netjson, 'interfaces', [])) + + @property + def wireless(self): + """ + Return all the wireless interfaces + """ + return wireless(get_copy(self.netjson, 'interfaces', [])) + + @property + def ebtables(self): + w = self.wireless[0] + ebtables_status = {'status': 'enabled'} + base = {} + if protocol(w) == 'none': + base.update(unencrypted(w)) + else: + base.update(encrypted(w)) + if self.netmode == 'bridge': + base['sys'].update({'fw': {'status': 'disabled'}}) + vlans = [] + _t = { + 'devname': '', + 'id': '', + 'status': '', + } + for v in self.vlan: + t = _t.copy() + name_and_id = v['name'].split('.') + t.update({ + 'devname': name_and_id[0], + 'id': name_and_id[1], + 'status': status(v), + }) + vlans.append(t) + if vlans: + base.setdefault('sys', {}) + base['sys']['vlan.status'] = 'enabled' + base['sys']['vlan'] = vlans + return [ebtables_status, base] + + def to_intermediate(self): + return (('ebtables', self.ebtables),) + + +class Gui(AirOsConverter): + netjson_key = 'gui' + + def to_intermediate(self): + original = get_copy(self.netjson, self.netjson_key, {}) + result = [ + { + 'language': original.get('language', 'en_US'), + }, + { + 'network': { + 'advanced': { + 'status': 'enabled', + } + } + } + ] + return (('gui', result),) + + +class Httpd(AirOsConverter): + + def to_intermediate(self): + result = [ + { + 'https': { + 'port': 443, + 'status': 'enabled', + }, + }, + { + 'port': 80, + 'session': {'timeout': 900}, + 'status': 'enabled', + } + ] + return (('httpd', result),) + + +class Igmpproxy(AirOsConverter): + + def to_intermediate(self): + result = {'status': 'disabled'} + if self.netmode == 'router': + result.update({'upstream': {'devname': ''}}) + return (('igmpproxy', [result]),) + + +class Iptables(AirOsConverter): + + _base = { + 'sys': { + 'portfw': {'status': 'disabled'}, + 'status': 'enabled', + }, + } + + _status = { + 'bridge': { + 'status': 'disabled', + }, + 'router': { + 'status': 'enabled', + } + } + + def bridge_intermediate(self): + base = self._base.copy() + iptables_status = self._status['bridge'].copy() + return [iptables_status, base] + + def router_intermediate(self): + base = self._base.copy() + iptables_status = self._status['router'].copy() + base['sys'].update({ + 'fw': {'status': 'disabled'}, + 'mgmt': [ + { + 'devname': 'br0', + 'status': 'enabled', + } + ], + 'mgmt.status': 'enabled', + }) + + return [iptables_status, base] + + def to_intermediate(self): + result = getattr(self, '{netmode}_intermediate'.format(netmode=self.netmode))() + return (('iptables', result),) + + +class Netconf(AirOsConverter): + netjson_key = 'interfaces' + + def to_intermediate(self): + result = [] + interfaces = [] + original = get_copy(self.netjson, self.netjson_key, []) + + for interface in original: + base = { + 'devname': interface['name'], + 'status': 'enabled', # can't disable interfaces + 'up': status(interface), + 'mtu': interface.get('mtu', 1500), + } + # handle interface type quirks + if interface['type'] == 'ethernet' and '.' not in interface['name']: + base['autoneg'] = autonegotiation(interface) + + base['flowcontrol'] = flowcontrol(interface) + + if interface['type'] == 'wireless': + base['devname'] = radio(interface) + + addresses = interface.get('addresses') + if addresses: + # for every address policy put a + # configuration + for addr in addresses: + temp = deepcopy(base) + if 'role' in addr: + temp['role'] = addr.get('role', '') + # handle explicit address policy + if addr['proto'] == 'dhcp': + temp['autoip'] = {'status': 'enabled'} + else: + temp.update(split_cidr(addr)) + interfaces.append(temp) + else: + # an interface without address + # is still valid with these defaults values + base['autoip'] = {'status': 'disabled'} + interfaces.append(base) + result.append(interfaces) + result.append({'status': 'enabled'}) + return (('netconf', result),) + + +class Netmode(AirOsConverter): + netjson_key = 'netmode' + + def to_intermediate(self): + result = [] + result.append({ + 'status': self.netmode, + }) + return (('netmode', result), ) + + +class Ntpclient(AirOsConverter): + netjson_key = 'ntp' + + def ntp_status(self, ntp): + if ntp.get('enabled', True): + return 'enabled' + else: + return 'disabled' + + def to_intermediate(self): + result = [] + servers = [] + original = get_copy(self.netjson, self.netjson_key, {}) + result.append({'status': self.ntp_status(original)}) + + for ntp in original.get('server', default_ntp_servers): + servers.append({ + 'server': ntp, + 'status': 'enabled', + }) + result.append(servers) + return (('ntpclient', result),) + + +class Pwdog(AirOsConverter): + + def to_intermediate(self): + result = [] + result.append({ + 'delay': 300, + 'period': 300, + 'retry': 3, + 'status': 'disabled', + }) + return (('pwdog', result),) + + +class Radio(AirOsConverter): + netjson_key = 'radios' + + @property + def radio(self): + return get_copy(self.netjson, self.netjson_key, []) + + @property + def wireless(self): + return wireless(get_copy(self.netjson, 'interfaces', [])) + + def to_intermediate(self): + result = [] + radios = [] + wireless = {radio(w): w for w in self.wireless} + for logic in self.radio: + w = wireless.get(logic['name']) + if w: + user_config = radio_available_mode[mode(w)](logic) + radios.append(user_config) + + result.append(radios) + result.append(radio_configuration) + return (('radio', result),) + + +class Resolv(AirOsConverter): + netjson_key = 'dns_servers' + + def host(self): + original = get_copy(self.netjson, 'general', {}) + return { + 'host': [{ + 'name': original.get('hostname', 'airos'), + 'status': 'enabled', + }], + } + + def nameserver(self): + result = [] + original = get_copy(self.netjson, self.netjson_key, []) + for nameserver in original: + result.append({ + 'ip': nameserver, + 'status': 'enabled', + }) + return {'nameserver': result} + + def to_intermediate(self): + result = [] + result.append(self.host()) + result.append(self.nameserver()) + result.append({'status': 'enabled'}) + return (('resolv', result),) + + +class Route(AirOsConverter): + netjson_key = 'routes' + + def default_routes(self): + def is_default_route(interface): + try: + t = [addr.get('gateway', '') for addr in interface['addresses']] + return any(t) + except KeyError: + return False + + result = [] + original = [x for x in get_copy(self.netjson, 'interfaces', []) if is_default_route(x)] + for interface in original: + for address in interface['addresses']: + try: + result.append({ + 'devname': interface['name'], + 'gateway': address['gateway'], + 'ip': '0.0.0.0', + 'netmask': 0, + 'status': 'enabled', + }) + except KeyError: + pass + return result + + def to_intermediate(self): + result = [] + routes = [] + routes = self.default_routes() + original = get_copy(self.netjson, self.netjson_key, []) + for r in original: + network = ip_interface(six.text_type(r['destination'])) + temp = {} + temp['ip'] = str(network.ip) + temp['netmask'] = str(network.netmask) + routes.append({ + 'gateway': r['next'], + 'ip': temp['ip'], + 'netmask': temp['netmask'], + 'status': 'enabled', + }) + result.append(routes) + result.append({'status': 'enabled'}) + return (('route', result),) + + +class Snmp(AirOsConverter): + netjson_key = 'general' + + def to_intermediate(self): + original = get_copy(self.netjson, self.netjson_key, {}) + result = [ + { + 'community': 'public', + 'contact': original.get('maintainer', ''), + 'location': original.get('location', ''), + 'status': 'enabled', + }, + ] + return (('snmp', result),) + + +class Sshd(AirOsConverter): + netjson_key = 'sshd' + + def to_intermediate(self): + def status(original, key='enabled'): + if original.get(key, True): + return 'enabled' + else: + return 'disabled' + + def to_key(x): + result = [] + for y in x: + result.append({ + 'status': status(y), + 'type': y['type'], + 'value': y['key'], + 'comment': y.get('comment', '') + }) + return result + + result = [] + original = get_copy(self.netjson, self.netjson_key, {}) + auth = {'passwd': status(original, 'password_auth')} + key = to_key(original.get('keys', [])) + if key: + auth.update({'key': key}) + + result.append({ + 'auth': auth, + 'port': original.get('port', 22), + 'status': status(original, 'enabled'), + }) + return (('sshd', result),) + + +class Syslog(AirOsConverter): + + def to_intermediate(self): + result = [] + + result.append({ + 'remote': { + 'port': 514, + 'status': 'disabled', + }, + 'status': 'enabled', + }) + return (('syslog', result),) + + +class System(AirOsConverter): + + def to_intermediate(self): + result = [] + result.append({ + 'airosx': { + 'prov': { + 'status': 'enabled', + }, + }, + 'cfg': { + 'version': 0, + }, + 'date': { + 'status': 'disabled', + }, + 'external': { + 'reset': 'enabled', + }, + 'timezone': 'GMT', + }) + return (('system', result),) + + +class Telnetd(AirOsConverter): + + def to_intermediate(self): + result = [] + result.append({ + 'port': 23, + 'status': 'disabled', + }) + return (('telnetd', result),) + + +class Tshaper(AirOsConverter): + + def to_intermediate(self): + return (('tshaper', [{'status': 'disabled'}]),) + + +class Unms(AirOsConverter): + + def to_intermediate(self): + return (('unms', [{'status': 'disabled'}]),) + + +class Update(AirOsConverter): + + def to_intermediate(self): + result = [] + result.append({'check': {'status': 'enabled'}}) + return (('update', result),) + + +class Upnpd(AirOsConverter): + @classmethod + def should_run_forward(cls, config): + if config.get('netmode', 'bridge') == 'bridge': + return False + else: + return True + + def to_intermediate(self): + return (('upnpd', [{'status': 'disabled'}]),) + + +class Users(AirOsConverter): + netjson_key = 'user' + + def key_derivation(self): + original = get_copy(self.netjson, self.netjson_key, {}) + return '$1${salt}${derivation}'.format(salt=original['salt'], derivation=original['password']) + + def to_intermediate(self): + result = [] + original = get_copy(self.netjson, self.netjson_key, {}) + result.append({'status': 'enabled'}) + result.append([ + { + 'name': original.get('name'), + 'password': self.key_derivation(), + 'status': 'enabled', + }, + ]) + return (('users', result),) + + +class Vlan(AirOsConverter): + netjson_key = 'interfaces' + + @property + def vlan(self): + return vlan(get_copy(self.netjson, self.netjson_key, [])) + + def to_intermediate(self): + result = [] + vlans = [] + for v in self.vlan: + vlans.append({ + 'comment': v.get('comment', ''), + 'devname': v['name'].split('.')[0], + 'id': v['name'].split('.')[1], + 'status': status(v), + }) + result.append(vlans) + result.append({'status': 'enabled'}) + return (('vlan', result),) + + +class Wireless(AirOsConverter): + netjson_key = 'interfaces' + + @property + def wireless(self): + """ + Return all the wireless interfaces + """ + return wireless(get_copy(self.netjson, 'interfaces', [])) + + def to_intermediate(self): + result = [] + wireless_list = [] + for w in self.wireless: + user_config = wireless_available_mode[mode(w)](w) + wireless_list.append(user_config) + result.append(wireless_list) + result.append({'status': 'enabled'}) + return (('wireless', result),) + + +class Wpasupplicant(AirOsConverter): + netjson_key = 'interfaces' + + @property + def wireless(self): + """ + Return all the wireless interfaces + """ + return wireless(get_copy(self.netjson, 'interfaces', [])) + + def _station_intermediate(self, original): + station_auth_protocols = available_mode_authentication['station'] + temp_dev = { + 'profile': 'AUTO', + 'status': 'enabled', + 'driver': 'madwifi', + 'devname': '', + } + result = [] + + if original: + head = original[0] + proto = protocol(head) + temp_dev['devname'] = radio(head) + network = station_auth_protocols[proto](head) + + if proto == 'none': + del temp_dev['driver'] + del temp_dev['devname'] + + result.append({ + 'device': [temp_dev], + 'profile': [ + { + 'name': 'AUTO', + 'network': [network, self.secondary_network()] + } + ] + }) + result.append({'status': 'enabled'}) + return (('wpasupplicant', result),) + + def _access_point_intermediate(self, original): + """ + Intermediate representation for ``access_point`` mode + """ + ap_auth_protocols = available_mode_authentication['access_point'] + temp_dev = { + 'profile': 'AUTO', + } + wpasupplicant_status = { + 'none': 'enabled', + 'wpa2_personal': 'disabled', + 'wpa2_enterprise': 'disabled', + } + result = [] + + if original: + head = original[0] + proto = protocol(head) + status = wpasupplicant_status[proto] + result.append({ + 'status': status + }) + temp_dev['status'] = status + network = ap_auth_protocols[proto](head) + profile = { + 'name': 'AUTO', + 'network': [network, self.secondary_network()], + } + + result.append({ + 'device': [temp_dev], + 'profile': [profile], + }) + return (('wpasupplicant', result),) + + def secondary_network(self): + """ + The default secondary network configuration + """ + return { + 'key_mgmt': [{'name': 'NONE'}], + 'priority': 2, + 'status': 'disabled', + } + + def to_intermediate(self): + try: + head = self.wireless[0] + # call either ``_station_intermediate`` or ``_access_point_intermediate`` + # and return the result + return getattr(self, '_%s_intermediate' % head['wireless']['mode'])(self.wireless) + except IndexError: + raise Warning('Zero wireless interface found') diff --git a/netjsonconfig/backends/airos/ebtables.py b/netjsonconfig/backends/airos/ebtables.py new file mode 100644 index 000000000..7d0d5ea03 --- /dev/null +++ b/netjsonconfig/backends/airos/ebtables.py @@ -0,0 +1,34 @@ +import copy + +from .interface import radio + +_base = { + 'sys': { + 'status': 'enabled', + }, +} + + +def encrypted(interface): + """ + Returns the configuration for ``ebtables.sys`` when encrypted + """ + base = copy.deepcopy(_base) + base['sys'].update({ + 'eap': [ + { + 'devname': radio(interface), + 'status': 'enabled', + } + ], + 'eap.status': 'enabled', + }) + return base + + +def unencrypted(interface): + """ + Returns the configuration for ``ebtables.sys`` + for an interface withouth encryption + """ + return {} diff --git a/netjsonconfig/backends/airos/interface.py b/netjsonconfig/backends/airos/interface.py new file mode 100644 index 000000000..66688d291 --- /dev/null +++ b/netjsonconfig/backends/airos/interface.py @@ -0,0 +1,129 @@ +from ipaddress import ip_interface + +from six import text_type + + +def autonegotiation(interface): + """ + Return the configuration for ``autoneg`` on interface + """ + if interface.get('autoneg'): + return 'enabled' + else: + return 'disabled' + + +def bridge(interfaces): + """ + Return the bridge interfaces from the interfaces list + """ + return [i for i in interfaces if i['type'] == 'bridge'] + + +def bssid(interface): + """ + Return the interface bssid + """ + return interface['wireless'].get('bssid', '') + + +def encryption(interface): + """ + Return the encryption dict for a wireless interface + """ + return interface['wireless'].get('encryption', {'protocol': 'none'}) + + +def flowcontrol(interface): + """ + Return the configuration for ``flowcontrol`` on interface + """ + if interface.get('flowcontrol'): + status = 'enabled' + else: + status = 'disabled' + return { + 'rx': { + 'status': status, + }, + 'tx': { + 'status': status, + }, + } + + +def hidden_ssid(interface): + """ + Return wether the ssid is hidden + """ + if interface['wireless'].get('hidden', False): + return 'enabled' + else: + return 'disabled' + + +def mode(interface): + """ + Return wireless interface mode + """ + return interface['wireless']['mode'] + + +def protocol(interface): + """ + Return wireless interface encryption + """ + return encryption(interface)['protocol'] + + +def psk(interface): + """ + Return the wpa2_personal psk + """ + return interface['wireless']['encryption']['key'] + + +def radio(interface): + """ + Return wireless interface's radio name + """ + return interface['wireless']['radio'] + + +def split_cidr(address): + """ + Return the address in dict format + """ + network = ip_interface(text_type('{addr}/{mask}'.format(addr=address['address'], mask=address['mask']))) + return {'ip': str(network.ip), 'netmask': str(network.netmask)} + + +def ssid(interface): + """ + Return the interface ssid + """ + return interface['wireless']['ssid'] + + +def stp(interface): + """ + Return wether the spanning tree protocol is enabled + """ + if interface.get('stp', False): + return 'enabled' + else: + return 'disabled' + + +def vlan(interfaces): + """ + Return the vlan interfaces from the interfaces list + """ + return [i for i in interfaces if '.' in i['name']] + + +def wireless(interfaces): + """ + Return the wireless interfaces from the interfaces list + """ + return [i for i in interfaces if i['type'] == 'wireless'] diff --git a/netjsonconfig/backends/airos/intermediate.py b/netjsonconfig/backends/airos/intermediate.py new file mode 100644 index 000000000..2320b6008 --- /dev/null +++ b/netjsonconfig/backends/airos/intermediate.py @@ -0,0 +1,106 @@ +from six import string_types +from six.moves import reduce + + +def flatten(elements): + """ + Flatten a list + elements :: List + return List + """ + if elements is not list: + return elements + else: + return reduce(lambda x, y: x + flatten(y), elements, []) + + +def shrink(configuration): + """ + configuration :: Dict + return Dict + """ + temp = {} + for key, value in configuration.items(): + if isinstance(value, string_types) or isinstance(value, int): + temp[key] = value + else: + # reduce to atom list + # as value could be dict or list + # enclose it in a flattened list + for child in intermediate_to_list(flatten([value])): + for child_key, child_value in child.items(): + nested_key = '{key}.{subkey}'.format(key=key, subkey=child_key) + temp[nested_key] = child_value + return temp + + +def intermediate_to_list(configuration): + """ + Explore the configuration tree and flatten where + possible with the following policy + - list -> prepend the list index to every item key + - dictionary -> prepend the father key to every key + + configuration :: List[Enum[Dict,List]] + return List[Dict] + + >>> intermediate_to_list([ + { + 'spam': { + 'eggs': 'spam and eggs' + } + } + ]) + >>> + [{ + 'spam.eggs' : 'spam and eggs' + ]} + + >>> intermediate_to_list([ + { + 'spam': { + 'eggs': 'spam and eggs' + } + }, + [ + { + 'henry': 'the first' + }, + { + 'jacob' : 'the second' + } + ] + ]) + >>> + [ + { + 'spam.eggs' : 'spam and eggs' + }, + { + '1.henry' : 'the first' + }, + { + '2.jacob' : 'the second' + } + ] + """ + + result = [] + + for element in configuration: + if isinstance(element, list): + for index, el in enumerate(element): + temp = {} + for key, value in el.items(): + temp['{i}.{key}'.format(i=index + 1, key=key)] = value + result = result + intermediate_to_list([temp]) + + elif isinstance(element, dict): + temp = {} + temp.update(shrink(element)) + result.append(temp) + + else: + raise Exception('malformed intermediate representation') + + return result diff --git a/netjsonconfig/backends/airos/radio.py b/netjsonconfig/backends/airos/radio.py new file mode 100644 index 000000000..c3e8dbd75 --- /dev/null +++ b/netjsonconfig/backends/airos/radio.py @@ -0,0 +1,98 @@ +""" +This configuration is for a radio in a ``station`` device without encryption +""" +radio_device_base = { + 'ack': {'auto': 'enabled'}, + 'ackdistance': 643, + 'acktimeout': 35, + 'ampdu': { + 'frames': 32, + 'status': 'enabled', + }, + 'antenna': { + 'gain': 3, + 'id': 2, + }, + 'atpc': { + 'sta.status': 'enabled', + 'status': 'disabled', + 'threshold': 36, + }, + 'cable': {'loss': 0}, + 'center': [{'freq': 0}], + 'chanbw': 0, + 'cmsbias': 0, + 'countrycode': 380, + 'cwm': { + 'enable': 0, + 'mode': 1, + }, + 'devname': 'ath0', + 'dfs': {'status': 'enabled'}, + 'freq': 0, + 'ieee_mode': 'auto', + 'low_txpower_mode': 'disabled', + 'mode': 'managed', + 'obey': 'enabled', + 'polling': 'enabled', + 'polling_11ac_11n_compat': 0, + 'polling_ff_dl_ratio': 50, + 'polling_ff_dur': 0, + 'polling_ff_timing': 0, + 'pollingnoack': 0, + 'pollingpri': 2, + 'ptpmode': 1, + 'reg_obey': 'enabled', + 'rx_sensitivity': -96, + 'scan_list': {'status': 'disabled'}, + 'scanbw': {'status': 'disabled'}, + 'status': 'enabled', # cannot disable + 'subsystemid': '0xe7f5', + 'txpower': 24, +} + +radio_configuration = { + 'status': 'enabled', + 'countrycode': 380, +} + + +def access_point(radio): + """ + Return the configuration for a radio device whose wireless + interface is in ``access_point`` mode + """ + base = radio_device_base.copy() + base.update({ + 'devname': radio['name'], + 'chanbw': 80, + 'ieee_mode': '11acvht80', + 'mode': 'master', + }) + return base + + +def station(radio): + """ + Return the configuration for a radio device whose wireless + interface is in ``station`` mode + """ + base = radio_device_base.copy() + base.update({ + 'devname': radio['name'], + 'chanbw': 0, + 'txpower': radio.get('tx_power', 24), + }) + return base + + +radio_available_mode = { + 'access_point': access_point, + 'station': station, +} + + +__all__ = [ + radio_available_mode, + radio_configuration, +] diff --git a/netjsonconfig/backends/airos/radius.py b/netjsonconfig/backends/airos/radius.py new file mode 100644 index 000000000..8b88f2110 --- /dev/null +++ b/netjsonconfig/backends/airos/radius.py @@ -0,0 +1,117 @@ +from .interface import encryption, mode, protocol + + +def ap_authentication(interface): + """ + Returns the ``radius.auth`` dict for ``access_point`` interface + """ + result = {} + proto = protocol(interface) + if proto == 'wpa2_personal': + result.update({ + 'status': 'disabled', + }) + elif proto == 'wpa2_enterprise': + enc = encryption(interface) + result.update({ + 'ip': enc.get('server', ''), + 'port': enc.get('port', 1812), + 'secret': enc.get('key', ''), + 'status': 'enabled', + }) + return result + + +def sta_authentication(interface): + """ + Returns the ``radius.auth`` dict for ``station`` interface + """ + result = {} + return result + + +_authentication_from_mode = { + 'access_point': ap_authentication, + 'station': sta_authentication, +} + + +def authentication(interface): + """ + returns the ``radius.auth`` dict + """ + result = { + 'port': 1812, + } + result.update(_authentication_from_mode[mode(interface)](interface)) + return result + + +def ap_accounting(interface): + """ + Returns the ``acct`` dict for ``access_point`` interfaces + """ + result = {} + if protocol(interface) == 'wpa2_enterprise': + enc = encryption(interface) + result.update({ + 'port': enc.get('acct_server_port', 1813), + 'ip': enc.get('acct_server', ''), + 'status': 'enabled', + }) + return result + + +def sta_accounting(interface): + """ + Returns the ``acct`` dict for ``station`` interfaces + """ + return {} + + +_accounting_from_mode = { + 'access_point': ap_accounting, + 'station': sta_accounting, +} + + +def accounting(interface): + """ + Returns the ``radius.acct`` dict + """ + result = { + 'port': 1813, + 'status': 'disabled', + } + result.update(_accounting_from_mode[mode(interface)](interface)) + return result + + +def radius_from_interface(interface): + """ + Return the ``radius`` configuration for + section ``aaa`` + """ + result = { + 'radius': { + 'auth': [ + authentication(interface), + ], + 'acct': [ + accounting(interface), + ], + } + } + if protocol(interface) != 'none' and mode(interface) != 'station': + result['radius'].update({ + 'macacl': { + 'status': 'disabled', + }, + }) + + return result + + +__all__ = [ + radius_from_interface, +] diff --git a/netjsonconfig/backends/airos/renderers.py b/netjsonconfig/backends/airos/renderers.py new file mode 100644 index 000000000..7512c7523 --- /dev/null +++ b/netjsonconfig/backends/airos/renderers.py @@ -0,0 +1,10 @@ +from ..base.renderer import BaseRenderer + + +class AirOsRenderer(BaseRenderer): + + def cleanup(self, output): + stripped = [ + a.strip() for a in output.splitlines() if a.strip() + ] + return '\n'.join(stripped) diff --git a/netjsonconfig/backends/airos/schema.py b/netjsonconfig/backends/airos/schema.py new file mode 100644 index 000000000..a572c994e --- /dev/null +++ b/netjsonconfig/backends/airos/schema.py @@ -0,0 +1,263 @@ +""" +AirOS specific JSON-Schema definition +""" +from ...schema import schema as default_schema +from ...utils import merge_config + + +""" +This defines a new property in the ``Interface``. + +The management interface is the one that exposes the +web interface + +It can be used on a single interface (ethernet, vlan) or +on a bridge +""" + +default_ntp_servers = [ + "0.pool.ntp.org", + "1.pool.ntp.org", + "2.pool.ntp.org", + "3.pool.ntp.org", +] + +override_schema = { + "type": "object", + "addtionalProperties": True, + "definitions": { + "base_address": { + "properties": { + "role": { + "type": "string", + "enum": [ + "none", + "mlan", + "wan", + "lan", + ], + "options": { + "enum_titles": [ + "None", + "Management interface (bridge mode)", + "Wan interface (router mode)", + "Lan interface (router mode)", + ] + }, + "default": "none", + "title": "Role", + "description": "Interface role", + "propertyOrder": 0, + } + } + }, + "encryption_wireless_property_ap": { + "properties": { + "encryption": { + "type": "object", + "title": "Encryption", + "required": [ + "protocol", + ], + "propertyOrder": 20, + "oneOf": [ + {"$ref": "#/definitions/encryption_none"}, + {"$ref": "#/definitions/encryption_wpa_personal"}, + {"$ref": "#/definitions/encryption_wpa_enterprise_ap"}, + ], + }, + }, + }, + "encryption_wireless_property_sta": { + "properties": { + "encryption": { + "type": "object", + "title": "Encryption", + "required": [ + "protocol", + ], + "propertyOrder": 20, + "oneOf": [ + {"$ref": "#/definitions/encryption_none"}, + {"$ref": "#/definitions/encryption_wpa_personal"}, + {"$ref": "#/definitions/encryption_wpa_enterprise_sta"}, + ], + }, + }, + }, + "interface_settings": { + "properties": { + "autoneg": { + "type": "boolean", + "default": False, + "title": "Auto negotiation", + "description": "Enable autonegotiation on interface", + "propertyOrder": 0, + }, + "flowcontrol": { + "type": "boolean", + "default": False, + "title": "Flow control", + "description": "Enable flow control on interface", + "propertyOrder": 0, + } + } + } + }, + "properties": { + "gui": { + "type": "object", + "properties": { + "language": { + "type": "string", + "default": "en_US", + "title": "Language", + "description": "Web interface language" + } + } + }, + "netmode": { + "enum": [ + "bridge", + "router", + ], + "default": "bridge", + "type": "string", + "title": "Network mode for device", + }, + "ntp": { + "type": "object", + "title": "NTP Settings", + "additionalProperties": True, + "propertyOrder": 8, + "properties": { + "enabled": { + "type": "boolean", + "title": "enable NTP client", + "default": True, + "format": "checkbox", + "propertyOrder": 1, + }, + "enable_server": { + "type": "boolean", + "title": "enable NTP server", + "default": False, + "format": "checkbox", + "propertyOrder": 2, + }, + "server": { + "title": "NTP Servers", + "description": "NTP server candidates", + "type": "array", + "uniqueItems": True, + "additionalItems": True, + "propertyOrder": 3, + "items": { + "title": "NTP server", + "type": "string", + "format": "hostname" + }, + "default": default_ntp_servers, + } + } + }, + "sshd": { + "type": "object", + "title": "SSHd settings", + "additionalProperties": True, + "required": [ + "port", + "enabled", + "password_auth", + ], + "properties": { + "port": { + "type": "integer", + "default": 22, + "title": "Port", + "description": "Port for sshd to listen on", + "propertyOrder": 0, + }, + "enabled": { + "type": "boolean", + "default": True, + "title": "Enable ssh server", + "format": "checkbox", + "propertyOrder": 1, + }, + "password_auth": { + "type": "boolean", + "default": True, + "title": "Enable password authentication", + "format": "checkbox", + "propertyOrder": 2, + }, + "keys": { + "type": "array", + "propertyOrder": 3, + "title": "Keys", + "description": "User keys", + "items": { + "type": "object", + "required": [ + "type", + "key", + ], + "properties": { + "type": { + "type": "string", + "title": "Key algorithm", + "default": "ssh-rsa", + }, + "key": { + "type": "string", + "title": "Key file content", + }, + "comment": { + "type": "string", + "default": "", + "title": "Comment", + }, + "enabled": { + "type": "boolean", + "default": True, + "title": "Enable key", + "format": "checkbox", + }, + } + } + }, + }, + }, + "user": { + "additionalProperties": True, + "properties": { + "name": { + "title": "User name", + "type": "string", + }, + "password": { + "title": "Hashed password for user", + "type": "string", + }, + "salt": { + "title": "Salt for hashing algorithm", + "type": "string", + }, + }, + "required": [ + "name", + "password", + "salt", + ], + }, + }, +} + +schema = merge_config(default_schema, override_schema) + +schema['definitions']['encryption_wireless_property_ap'] = \ + override_schema['definitions']['encryption_wireless_property_ap'] + +schema['definitions']['encryption_wireless_property_sta'] = \ + override_schema['definitions']['encryption_wireless_property_sta'] diff --git a/netjsonconfig/backends/airos/templates/airos.jinja2 b/netjsonconfig/backends/airos/templates/airos.jinja2 new file mode 100644 index 000000000..9bdf9b2fc --- /dev/null +++ b/netjsonconfig/backends/airos/templates/airos.jinja2 @@ -0,0 +1,13 @@ +{% for namespace, block in data.items() %} + {% if namespace != 'netmode' %} + {% for element in block %} + {% for k, v in element.items() %} + {{ namespace }}.{{ k }}={{ v }} + {% endfor %} + {% endfor %} + {% else %} + {% for element in block %} + netmode={{ element['status'] }} + {% endfor %} + {% endif %} +{% endfor %} diff --git a/netjsonconfig/backends/airos/wireless.py b/netjsonconfig/backends/airos/wireless.py new file mode 100644 index 000000000..153b7817e --- /dev/null +++ b/netjsonconfig/backends/airos/wireless.py @@ -0,0 +1,71 @@ +from .interface import bssid, hidden_ssid, radio, ssid + +_wireless_base = { + 'addmtikie': 'enabled', + 'devname': '', + 'hide_ssid': '', + 'l2_isolation': 'disabled', + 'mac_acl': { + 'policy': 'allow', + 'status': 'disabled', + }, + 'mcast': {'enhance': 0}, + 'rate': { + 'auto': 'enabled', + 'mcs': -1, + }, + 'security': {'type': 'none'}, + 'signal_led1': 75, + 'signal_led2': 50, + 'signal_led3': 25, + 'signal_led4': 15, + 'signal_led_status': 'enabled', + 'ssid': '', + 'status': '', + 'wds': {'status': 'enabled'}, +} + + +def status(interface): + """ + Return the ``wireless`` status + """ + if interface.get('disabled'): + return 'disabled' + else: + return 'enabled' + + +def access_point(wlan): + """ + Return the configuration for a wireless lan in ``access_point`` mode + """ + base = _wireless_base.copy() + base.update({ + 'devname': radio(wlan), + 'hide_ssid': hidden_ssid(wlan), + 'ssid': ssid(wlan), + 'status': status(wlan), + }) + return base + + +def station(wlan): + """ + Return the configuration for a wireless lan in ``station`` mode + """ + base = _wireless_base.copy() + base.update({ + 'ap': bssid(wlan), + 'devname': radio(wlan), + 'hide_ssid': hidden_ssid(wlan), + 'ssid': ssid(wlan), + 'status': status(wlan), + }) + return base + + +wireless_available_mode = { + 'access_point': access_point, + 'station': station, +} diff --git a/netjsonconfig/backends/airos/wpasupplicant.py b/netjsonconfig/backends/airos/wpasupplicant.py new file mode 100644 index 000000000..25c1374a8 --- /dev/null +++ b/netjsonconfig/backends/airos/wpasupplicant.py @@ -0,0 +1,102 @@ +from .interface import bssid, encryption, ssid + + +def ap_no_encryption(interface): + """ + Returns the wpasupplicant.profile.1.network + for encryption None as the intermediate dict + """ + return { + 'ssid': ssid(interface), + 'priority': 100, + 'key_mgmt': [{'name': 'NONE'}], + } + + +def ap_wpa2_personal(interface): + """ + Returns the wpasupplicant.profile.1.network + for wpa2_personal as the indernediate dict + in ``access_point`` mode + """ + base = ap_no_encryption(interface) + base.update({'psk': encryption(interface)['key']}) + return base + + +def ap_wpa2_enterprise(interface): + """ + Returns the wpasupplicant.profile.1.network + for wpa2_personal as the indernediate dict + in ``access_point`` mode + """ + return ap_no_encryption(interface) + + +def sta_no_encryption(interface): + """ + Returns the wpasupplicant.profile.1.network + for encryption None as the intermediate dict + in ``station`` mode + """ + return { + 'ssid': ssid(interface), + 'priority': 100, + 'key_mgmt': [{'name': 'NONE'}], + } + + +def sta_wpa2_personal(interface): + """ + Returns the wpasupplicant.profile.1.network + for wpa2_personal as the indernediate dict + in ``station`` mode + """ + base = sta_no_encryption(interface) + base.update({ + 'psk': encryption(interface)['key'], + 'eap': [{'status': 'disabled'}], + 'key_mgmt': [{'name': 'WPA-PSK'}], + 'pairwise': [{'name': 'CCMP'}], + 'phase2=auth': 'MSCHAPV2', + 'proto': [{'name': 'RSN'}], + }) + return base + + +def sta_wpa2_enterprise(interface): + """ + Returns the wpasupplicant.profile.1.network + for wpa2_enterprise as the intermediate dict + """ + base = ap_no_encryption(interface) + base.update({ + 'bssid': bssid(interface), + 'phase2=auth': 'MSCHAPV2', + 'eap': [ + { + 'name': 'TTLS', + 'status': 'enabled', + }, + ], + 'password': encryption(interface)['password'], + 'identity': encryption(interface)['identity'], + 'pairwise': [{'name': 'CCMP'}], + 'proto': [{'name': 'RSN'}], + 'key_mgmt': [{'name': 'WPA-EAP'}], + }) + return base + + +available_mode_authentication = { + 'access_point': { + 'none': ap_no_encryption, + 'wpa2_personal': ap_wpa2_personal, + 'wpa2_enterprise': ap_wpa2_enterprise, + }, + 'station': { + 'none': sta_no_encryption, + 'wpa2_personal': sta_wpa2_personal, + 'wpa2_enterprise': sta_wpa2_enterprise, + }, +} diff --git a/tests/airos/__init__.py b/tests/airos/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/airos/mock.py b/tests/airos/mock.py new file mode 100644 index 000000000..e27ab5509 --- /dev/null +++ b/tests/airos/mock.py @@ -0,0 +1,310 @@ +from unittest import TestCase + +from netjsonconfig import AirOs +from netjsonconfig.backends.airos.airos import to_ordered_list +from netjsonconfig.backends.airos.converters import (Aaa, Bridge, Dhcpc, + Discovery, Dyndns, + Ebtables, Gui, Httpd, + Igmpproxy, Iptables, + Netconf, Netmode, + Ntpclient, Pwdog, Radio, + Resolv, Route, Snmp, Sshd, + Syslog, System, Telnetd, + Tshaper, Unms, Update, + Upnpd, Users, Vlan, + Wireless, Wpasupplicant) + + +class ConverterTest(TestCase): + """ + Test case specific for intermediate configuration checks + + The intermediate configuration is a dict-like object with + section names as keys and a list of configuration values + as values + """ + maxDiff = 1000 + + def assertEqualConfig(self, a, b): + """ + Test that the content of two list is the equal + element wise + + This provides smaller, more specific, reports as it will trigger + failure for differently ordered elements or the element + content + + If an element fails the assertion will be the only one printed + """ + for (a, b) in zip(a, to_ordered_list(b)): + self.assertEqual(a, b) + + +class AaaAirOs(AirOs): + """ + Mock backend with converter for radius authentication + """ + converters = [ + Aaa, + ] + + +class BridgeAirOs(AirOs): + """ + Mock backend with converter for bridge interface + """ + converters = [ + Bridge, + ] + + +class DhcpcAirOs(AirOs): + """ + Mock backend with converter for network hardware discovery + """ + converters = [ + Dhcpc, + ] + + +class DiscoveryAirOs(AirOs): + """ + Mock backend with converter for network hardware discovery + """ + converters = [ + Discovery, + ] + + +class DyndnsAirOs(AirOs): + """ + Mock backend with converter for dynamic dns capabilities + """ + converters = [ + Dyndns, + ] + + +class EbtablesAirOs(AirOs): + """ + Mock backend with converter for ebtables + """ + converters = [ + Ebtables, + ] + + +class GuiAirOs(AirOs): + """ + Mock backend with converter for web interface settings + """ + converters = [ + Gui, + ] + + +class HttpdAirOs(AirOs): + """ + Mock backend with converter for web server + """ + converters = [ + Httpd, + ] + + +class IgmpproxyAirOs(AirOs): + """ + Mock backend with converter for igmpproxy + """ + converters = [ + Igmpproxy, + ] + + +class IptablesAirOs(AirOs): + """ + Mock backend with converter for iptables + """ + converters = [ + Iptables, + ] + + +class NetconfAirOs(AirOs): + """ + Mock backend with converter for network configuration + """ + converters = [ + Netconf, + ] + + +class NetmodeAirOs(AirOs): + """ + Mock backend with converter for network mode + """ + converters = [ + Netmode, + ] + + +class NtpclientAirOs(AirOs): + """ + Mock backend with converter for ntp settings + """ + converters = [ + Ntpclient, + ] + + +class PwdogAirOs(AirOs): + """ + Mock backend with converter for ping watchdog settings + """ + converters = [ + Pwdog, + ] + + +class RadioAirOs(AirOs): + """ + Mock backend with converter for radio settings + """ + converters = [ + Radio, + ] + + +class ResolvAirOs(AirOs): + """ + Mock backend with converter for network resolver + """ + converters = [ + Resolv, + ] + + +class RouteAirOs(AirOs): + """ + Mock backend with converter for static routes + """ + converters = [ + Route, + ] + + +class SnmpAirOs(AirOs): + """ + Mock backend with converter for simple network management protocol + """ + converters = [ + Snmp, + ] + + +class SshdAirOs(AirOs): + """ + Mock backend with converter for ssh daemon settings + """ + converters = [ + Sshd, + ] + + +class SyslogAirOs(AirOs): + """ + Mock backend with converter for remote logging + """ + converters = [ + Syslog, + ] + + +class SystemAirOs(AirOs): + """ + Mock backend with converter for system settings + """ + converters = [ + System, + ] + + +class TelnetdAirOs(AirOs): + """ + Mock backend with converter for telnet daemon settings + """ + converters = [ + Telnetd, + ] + + +class TshaperAirOs(AirOs): + """ + Mock backend with converter for tshaper + """ + converters = [ + Tshaper, + ] + + +class UnmsAirOs(AirOs): + """ + Mock backend with converter for unms + """ + converters = [ + Unms, + ] + + +class UpdateAirOs(AirOs): + """ + Mock backend with converter for update + """ + converters = [ + Update, + ] + + +class UpnpdAirOs(AirOs): + """ + Mock backend with converter for updnd daemon + """ + converters = [ + Upnpd, + ] + + +class UsersAirOs(AirOs): + """ + Mock backend with converter for users settings + """ + converters = [ + Users, + ] + + +class VlanAirOs(AirOs): + """ + Mock backend with converter for vlan settings + """ + converters = [ + Vlan, + ] + + +class WirelessAirOs(AirOs): + """ + Mock backend with converter for wireless settings + """ + converters = [ + Wireless, + ] + + +class WpasupplicantAirOs(AirOs): + """ + Mock backend with converter for wpasupplicant settings + """ + converters = [ + Wpasupplicant, + ] diff --git a/tests/airos/test_aaa.py b/tests/airos/test_aaa.py new file mode 100644 index 000000000..cb9e87e52 --- /dev/null +++ b/tests/airos/test_aaa.py @@ -0,0 +1,272 @@ +from .mock import AaaAirOs, ConverterTest + + +class TestAaaConverterAccess(ConverterTest): + + backend = AaaAirOs + + def test_ap_no_authentication(self): + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "wireless": { + "mode": "access_point", + "radio": "ath0", + "ssid": "i-like-pasta", + "encryption": { + "protocol": "none" + } + }, + }, + { + "type": "bridge", + "name": "br0", + "bridge_members": [ + "wlan0", + ], + }, + ], + }) + o.to_intermediate() + expected = [ + { + 'status': 'disabled', + }, + { + '1.radius.acct.1.port': 1813, + '1.radius.acct.1.status': 'disabled', + '1.radius.auth.1.port': 1812, + '1.status': 'disabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['aaa'], expected) + + def test_ap_psk_authentication(self): + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "wireless": { + "mode": "access_point", + "radio": "ath0", + "ssid": "i-like-pasta", + "encryption": { + "protocol": "wpa2_personal", + "key": "and-pizza-too", + }, + }, + }, + { + "type": "bridge", + "name": "br0", + "bridge_members": [ + "wlan0", + ], + }, + ], + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + '1.radius.acct.1.port': 1813, + '1.radius.acct.1.status': 'disabled', + '1.radius.auth.1.port': 1812, + '1.radius.auth.1.status': 'disabled', + '1.status': 'enabled', + '1.wpa.psk': 'and-pizza-too', + '1.radius.macacl.status': 'disabled', + '1.ssid': 'i-like-pasta', + '1.br.devname': 'br0', # only in bridge mode? + '1.devname': 'ath0', + '1.driver': 'madwifi', + '1.wpa.1.pairwise': 'CCMP', + '1.wpa.key.1.mgmt': 'WPA-PSK', + '1.wpa.mode': 2, + } + ] + self.assertEqualConfig(o.intermediate_data['aaa'], expected) + + def test_ap_eap_authentication(self): + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "wireless": { + "mode": "access_point", + "radio": "ath0", + "ssid": "i-like-pasta", + "encryption": { + "protocol": "wpa2_enterprise", + "key": "secret-radius-key", + "server": "192.168.1.1", + "acct_server": "192.168.1.2", + }, + }, + }, + { + "type": "bridge", + "name": "br0", + "bridge_members": [ + "wlan0", + ], + }, + ], + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + '1.br.devname': 'br0', + '1.devname': 'ath0', + '1.driver': 'madwifi', + '1.radius.acct.1.port': 1813, + '1.radius.acct.1.status': 'enabled', + '1.radius.acct.1.ip': '192.168.1.2', + '1.radius.auth.1.ip': '192.168.1.1', + '1.radius.auth.1.port': 1812, + '1.radius.auth.1.secret': 'secret-radius-key', + '1.radius.auth.1.status': 'enabled', + '1.radius.macacl.status': 'disabled', + '1.ssid': 'i-like-pasta', + '1.status': 'enabled', + '1.wpa.1.pairwise': 'CCMP', + '1.wpa.key.1.mgmt': 'WPA-EAP', + '1.wpa.mode': 2, + } + ] + self.assertEqualConfig(o.intermediate_data['aaa'], expected) + + +class TestAaaConverterStation(ConverterTest): + + backend = AaaAirOs + + def test_sta_no_authentication(self): + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "wireless": { + "mode": "station", + "radio": "ath0", + "ssid": "i-like-pasta", + "bssid": "00:11:22:33:44:55", + "encryption": { + "protocol": "none", + }, + }, + }, + { + "type": "bridge", + "name": "br0", + "bridge_members": [ + "wlan0", + ], + }, + ], + }) + o.to_intermediate() + expected = [ + { + 'status': 'disabled', + }, + { + '1.radius.acct.1.port': 1813, + '1.radius.acct.1.status': 'disabled', + '1.radius.auth.1.port': 1812, + '1.status': 'disabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['aaa'], expected) + + def test_sta_psk_authentication(self): + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "wireless": { + "mode": "station", + "radio": "ath0", + "ssid": "i-like-pasta", + "bssid": "00:11:22:33:44:55", + "encryption": { + "protocol": "wpa2_personal", + "key": "and-pizza-too", + }, + }, + }, + { + "type": "bridge", + "name": "br0", + "bridge_members": [ + "wlan0", + ], + }, + ], + }) + o.to_intermediate() + expected = [ + { + 'status': 'disabled', + }, + { + '1.radius.acct.1.port': 1813, + '1.radius.acct.1.status': 'disabled', + '1.radius.auth.1.port': 1812, + '1.status': 'disabled', + '1.wpa.psk': 'and-pizza-too', + }, + ] + self.assertEqualConfig(o.intermediate_data['aaa'], expected) + + def test_sta_eap_authentication(self): + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "wireless": { + "mode": "station", + "radio": "ath0", + "ssid": "i-like-pasta", + "bssid": "00:11:22:33:44:55", + "encryption": { + "protocol": "wpa2_enterprise", + "identity": "some-fake-identity", + "password": "password1234", + }, + }, + }, + { + "type": "bridge", + "name": "br0", + "bridge_members": [ + "wlan0", + ], + }, + ], + }) + o.to_intermediate() + expected = [ + { + 'status': 'disabled', + }, + { + '1.radius.acct.1.port': 1813, + '1.radius.acct.1.status': 'disabled', + '1.radius.auth.1.port': 1812, + '1.status': 'disabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['aaa'], expected) diff --git a/tests/airos/test_bridge.py b/tests/airos/test_bridge.py new file mode 100644 index 000000000..f232693bc --- /dev/null +++ b/tests/airos/test_bridge.py @@ -0,0 +1,191 @@ +from .mock import BridgeAirOs, ConverterTest + + +class TestBridgeConverter(ConverterTest): + """ + tests for backends.airos.renderers.SystemRenderer + """ + backend = BridgeAirOs + + def test_active_bridge(self): + + o = self.backend({ + "interfaces": [ + { + "type": "ethernet", + "name": "eth0", + "disabled": False, + }, + { + "type": "ethernet", + "name": "eth1", + "disabled": False, + }, + { + "type": "bridge", + "name": "br0", + "bridge_members": [ + "eth0", + "eth1", + ], + "disabled": False, + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.comment': '', + '1.devname': 'br0', + '1.port.1.devname': 'eth0', + '1.port.1.status': 'enabled', + '1.port.2.devname': 'eth1', + '1.port.2.status': 'enabled', + '1.status': 'enabled', + '1.stp.status': 'disabled' + }, + { + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['bridge'], expected) + + def test_disabled_bridge(self): + o = self.backend({ + "interfaces": [ + { + "type": "ethernet", + "name": "eth0", + "disabled": False, + }, + { + "type": "ethernet", + "name": "eth1", + "disabled": False, + }, + { + "type": "bridge", + "name": "br0", + "bridge_members": [ + "eth0", + "eth1", + ], + "disabled": True, + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.comment': '', + '1.devname': 'br0', + '1.port.1.devname': 'eth0', + '1.port.1.status': 'enabled', + '1.port.2.devname': 'eth1', + '1.port.2.status': 'enabled', + '1.status': 'disabled', + '1.stp.status': 'disabled' + }, + { + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['bridge'], expected) + + def test_many_bridges(self): + o = self.backend({ + "interfaces": [ + { + "type": "ethernet", + "name": "eth0", + "disabled": False, + }, + { + "type": "ethernet", + "name": "eth1", + "disabled": False, + }, + { + "type": "bridge", + "name": "br0", + "bridge_members": [ + "eth0", + "eth1", + ], + "disabled": True, + }, + { + "type": "ethernet", + "name": "eth2", + "disabled": False, + }, + { + "type": "ethernet", + "name": "eth3", + "disabled": False, + }, + { + "type": "bridge", + "name": "br1", + "bridge_members": [ + "eth2", + "eth3", + ], + "disabled": False, + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.comment': '', + '1.devname': 'br0', + '1.port.1.devname': 'eth0', + '1.port.1.status': 'enabled', + '1.port.2.devname': 'eth1', + '1.port.2.status': 'enabled', + '1.status': 'disabled', + '1.stp.status': 'disabled' + }, + { + '2.comment': '', + '2.devname': 'br1', + '2.port.1.devname': 'eth2', + '2.port.1.status': 'enabled', + '2.port.2.devname': 'eth3', + '2.port.2.status': 'enabled', + '2.status': 'enabled', + '2.stp.status': 'disabled' + }, + { + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['bridge'], expected) + + def test_no_bridge(self): + o = self.backend({ + "interfaces": [ + { + "type": "ethernet", + "name": "eth0", + "disabled": False, + }, + { + "type": "ethernet", + "name": "eth1", + "disabled": False, + }, + ] + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['bridge'], expected) diff --git a/tests/airos/test_dhcpc.py b/tests/airos/test_dhcpc.py new file mode 100644 index 000000000..298602395 --- /dev/null +++ b/tests/airos/test_dhcpc.py @@ -0,0 +1,36 @@ +from .mock import ConverterTest, DhcpcAirOs + + +class TestDhcpcConverter(ConverterTest): + + backend = DhcpcAirOs + + def test_bridge(self): + o = self.backend({ + 'netmode': 'bridge', + }) + o.to_intermediate() + + with self.assertRaises(KeyError): + o.intermediate_data['dhcpc'] + + def test_router(self): + o = self.backend({ + 'netmode': 'router', + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + [ + { + 'devname': 'br0', + 'fallback': '192.168.10.1', + 'fallback_netmask': '255.255.255.0', + 'status': 'enabled', + }, + ] + ] + + self.assertEqualConfig(o.intermediate_data['dhcpc'], expected) diff --git a/tests/airos/test_discovery.py b/tests/airos/test_discovery.py new file mode 100644 index 000000000..2d7e9e453 --- /dev/null +++ b/tests/airos/test_discovery.py @@ -0,0 +1,20 @@ +from .mock import ConverterTest, DiscoveryAirOs + + +class TestDiscoveryConverter(ConverterTest): + + backend = DiscoveryAirOs + + def test_discovery_key(self): + o = self.backend({ + "general": {} + }) + o.to_intermediate() + expected = [ + { + 'cdp.status': 'enabled', + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['discovery'], expected) diff --git a/tests/airos/test_dyndns.py b/tests/airos/test_dyndns.py new file mode 100644 index 000000000..c45872273 --- /dev/null +++ b/tests/airos/test_dyndns.py @@ -0,0 +1,19 @@ +from .mock import ConverterTest, DyndnsAirOs + + +class TestDyndnsConverter(ConverterTest): + + backend = DyndnsAirOs + + def test_Dyndns_key(self): + o = self.backend({ + "general": {} + }) + o.to_intermediate() + expected = [ + { + 'status': 'disabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['dyndns'], expected) diff --git a/tests/airos/test_ebtables.py b/tests/airos/test_ebtables.py new file mode 100644 index 000000000..de92700fd --- /dev/null +++ b/tests/airos/test_ebtables.py @@ -0,0 +1,373 @@ +from .mock import ConverterTest, EbtablesAirOs + + +class EbtablesConverterBridge(ConverterTest): + backend = EbtablesAirOs + + def test_station_none(self): + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'radio': 'ath0', + 'ssid': 'ubnt', + 'bssid': '00:11:22:33:44:55', + } + } + ] + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + 'sys.fw.status': 'disabled', + 'sys.status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['ebtables'], expected) + + def test_station_psk(self): + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'radio': 'ath0', + 'ssid': 'ubnt', + 'bssid': '00:11:22:33:44:55', + 'encryption': { + 'protocol': 'wpa2_personal', + 'key': 'changeme', + } + } + } + ] + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + 'sys.eap.1.devname': 'ath0', + 'sys.eap.1.status': 'enabled', + 'sys.eap.status': 'enabled', + 'sys.fw.status': 'disabled', + 'sys.status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['ebtables'], expected) + + def test_station_eap(self): + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'radio': 'ath0', + 'ssid': 'ubnt', + 'bssid': '00:11:22:33:44:55', + 'encryption': { + 'protocol': 'wpa2_enterprise', + 'identity': 'name@domain.com', + 'key': 'changeme', + } + } + } + ] + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + 'sys.eap.1.devname': 'ath0', + 'sys.eap.1.status': 'enabled', + 'sys.eap.status': 'enabled', + 'sys.fw.status': 'disabled', + 'sys.status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['ebtables'], expected) + + def test_access_none(self): + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'access_point', + 'radio': 'ath0', + 'ssid': 'ubnt', + } + } + ] + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + 'sys.fw.status': 'disabled', + 'sys.status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['ebtables'], expected) + + def test_access_psk(self): + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'access_point', + 'radio': 'ath0', + 'ssid': 'ubnt', + 'encryption': { + 'protocol': 'wpa2_personal', + 'key': 'changeme', + } + } + } + ] + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + 'sys.eap.1.devname': 'ath0', + 'sys.eap.1.status': 'enabled', + 'sys.eap.status': 'enabled', + 'sys.fw.status': 'disabled', + 'sys.status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['ebtables'], expected) + + def test_access_eap(self): + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'access_point', + 'radio': 'ath0', + 'ssid': 'ubnt', + 'encryption': { + 'protocol': 'wpa2_enterprise', + 'server': '192.168.1.1', + 'key': 'changeme', + } + } + } + ] + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + 'sys.eap.1.devname': 'ath0', + 'sys.eap.1.status': 'enabled', + 'sys.eap.status': 'enabled', + 'sys.fw.status': 'disabled', + 'sys.status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['ebtables'], expected) + + +class EbtablesConverterRouter(ConverterTest): + backend = EbtablesAirOs + + def test_station_none(self): + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'radio': 'ath0', + 'ssid': 'ubnt', + 'bssid': '00:11:22:33:44:55', + } + } + ], + "netmode": "router", + }) + o.to_intermediate() + expected = [] + self.assertEqualConfig(o.intermediate_data['ebtables'], expected) + + def test_station_psk(self): + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'radio': 'ath0', + 'ssid': 'ubnt', + 'bssid': '00:11:22:33:44:55', + 'encryption': { + 'protocol': 'wpa2_personal', + 'key': 'changeme', + } + } + } + ], + "netmode": "router", + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + 'sys.eap.1.devname': 'ath0', + 'sys.eap.1.status': 'enabled', + 'sys.eap.status': 'enabled', + 'sys.status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['ebtables'], expected) + + def test_station_eap(self): + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'radio': 'ath0', + 'ssid': 'ubnt', + 'bssid': '00:11:22:33:44:55', + 'encryption': { + 'protocol': 'wpa2_enterprise', + 'identity': 'name@domain.com', + 'key': 'changeme', + } + } + } + ], + "netmode": "router", + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + 'sys.eap.1.devname': 'ath0', + 'sys.eap.1.status': 'enabled', + 'sys.eap.status': 'enabled', + 'sys.status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['ebtables'], expected) + + def test_access_none(self): + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'access_point', + 'radio': 'ath0', + 'ssid': 'ubnt', + } + } + ], + "netmode": "router", + }) + o.to_intermediate() + expected = [] + self.assertEqualConfig(o.intermediate_data['ebtables'], expected) + + def test_access_psk(self): + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'access_point', + 'radio': 'ath0', + 'ssid': 'ubnt', + 'encryption': { + 'protocol': 'wpa2_personal', + 'key': 'changeme', + } + } + } + ], + "netmode": "router", + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + 'sys.eap.1.devname': 'ath0', + 'sys.eap.1.status': 'enabled', + 'sys.eap.status': 'enabled', + 'sys.status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['ebtables'], expected) + + def test_access_eap(self): + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'access_point', + 'radio': 'ath0', + 'ssid': 'ubnt', + 'encryption': { + 'protocol': 'wpa2_enterprise', + 'server': '192.168.1.1', + 'key': 'radius-change-me', + } + } + } + ], + "netmode": "router", + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + 'sys.eap.1.devname': 'ath0', + 'sys.eap.1.status': 'enabled', + 'sys.eap.status': 'enabled', + 'sys.status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['ebtables'], expected) diff --git a/tests/airos/test_gui.py b/tests/airos/test_gui.py new file mode 100644 index 000000000..3444c482b --- /dev/null +++ b/tests/airos/test_gui.py @@ -0,0 +1,40 @@ +from .mock import ConverterTest, GuiAirOs + + +class TestGuiConverter(ConverterTest): + + backend = GuiAirOs + + def test_gui_key(self): + o = self.backend({ + 'gui': {}, + }) + o.to_intermediate() + expected = [ + { + 'language': 'en_US', + }, + { + 'network.advanced.status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['gui'], expected) + + def test_language(self): + o = self.backend({ + 'gui': { + 'language': 'it_IT', + }, + }) + o.to_intermediate() + expected = [ + { + 'language': 'it_IT', + }, + { + 'network.advanced.status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['gui'], expected) diff --git a/tests/airos/test_httpd.py b/tests/airos/test_httpd.py new file mode 100644 index 000000000..178e081f3 --- /dev/null +++ b/tests/airos/test_httpd.py @@ -0,0 +1,25 @@ +from .mock import ConverterTest, HttpdAirOs + + +class TestHttpdConverter(ConverterTest): + + backend = HttpdAirOs + + def test_httpd_key(self): + o = self.backend({ + "general": {} + }) + o.to_intermediate() + expected = [ + { + 'https.port': 443, + 'https.status': 'enabled', + }, + { + 'port': 80, + 'session.timeout': 900, + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['httpd'], expected) diff --git a/tests/airos/test_igmpproxy.py b/tests/airos/test_igmpproxy.py new file mode 100644 index 000000000..a2e2933f8 --- /dev/null +++ b/tests/airos/test_igmpproxy.py @@ -0,0 +1,15 @@ +from .mock import ConverterTest, IgmpproxyAirOs + + +class Igmpproxyconverter(ConverterTest): + backend = IgmpproxyAirOs + + def test_ebtables(self): + o = self.backend({}) + o.to_intermediate() + expected = [ + { + 'status': 'disabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['igmpproxy'], expected) diff --git a/tests/airos/test_interface.py b/tests/airos/test_interface.py new file mode 100644 index 000000000..0776ad3ca --- /dev/null +++ b/tests/airos/test_interface.py @@ -0,0 +1,157 @@ +from unittest import TestCase + +from netjsonconfig.backends.airos.interface import (autonegotiation, bssid, + encryption, flowcontrol, + hidden_ssid, stp) + + +class InterfaceTest(TestCase): + def test_autonegotiation(self): + enabled = { + 'type': "ethernet", + 'name': "eth0", + 'autoneg': True, + } + disabled = { + 'type': "ethernet", + 'name': "eth0", + 'autoneg': False, + } + missing = { + 'type': "ethernet", + 'name': "eth0", + } + self.assertEqual(autonegotiation(enabled), 'enabled') + self.assertEqual(autonegotiation(disabled), 'disabled') + self.assertEqual(autonegotiation(missing), 'disabled') + + def test_bssid(self): + present = { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'ssid': 'ubnt', + 'bssid': '00:11:22:33:44:55', + } + } + missing = { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'ssid': 'ubnt', + } + } + + self.assertEqual(bssid(present), '00:11:22:33:44:55') + self.assertEqual(bssid(missing), '') + + def test_encryption(self): + present = { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'ssid': 'ubnt', + 'encryption': { + 'protocol': 'wpa2_personal', + 'password': 'changeme', + }, + } + } + missing = { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'ssid': 'ubnt', + } + } + self.assertEqual(encryption(present), {'protocol': 'wpa2_personal', 'password': 'changeme'}) + self.assertEqual(encryption(missing), {'protocol': 'none'}) + + def test_flowcontrol(self): + enabled = { + 'type': "ethernet", + 'name': "eth0", + 'flowcontrol': True, + } + disabled = { + 'type': "ethernet", + 'name': "eth0", + 'flowcontrol': False, + } + missing = { + 'type': "ethernet", + 'name': "eth0", + } + expected_enabled = { + 'rx': { + 'status': 'enabled', + }, + 'tx': { + 'status': 'enabled', + }, + } + expected_disabled = { + 'rx': { + 'status': 'disabled', + }, + 'tx': { + 'status': 'disabled', + }, + } + self.assertEqual(flowcontrol(enabled), expected_enabled) + self.assertEqual(flowcontrol(disabled), expected_disabled) + self.assertEqual(flowcontrol(missing), expected_disabled) + + def test_hidden_ssid(self): + enabled = { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'ssid': 'ubnt', + 'hidden': True, + } + } + disabled = { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'ssid': 'ubnt', + 'hidden': False, + } + } + missing = { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'mode': 'station', + 'ssid': 'ubnt', + } + } + self.assertEqual(hidden_ssid(enabled), 'enabled') + self.assertEqual(hidden_ssid(disabled), 'disabled') + self.assertEqual(hidden_ssid(missing), 'disabled') + + def test_stp(self): + enabled = { + 'type': 'bridge', + 'name': 'br0', + 'stp': True, + } + disabled = { + 'type': 'bridge', + 'name': 'br0', + 'stp': False, + } + missing = { + 'type': 'bridge', + 'name': 'br0', + } + self.assertEqual(stp(enabled), 'enabled') + self.assertEqual(stp(disabled), 'disabled') + self.assertEqual(stp(missing), 'disabled') diff --git a/tests/airos/test_intermediate.py b/tests/airos/test_intermediate.py new file mode 100644 index 000000000..d8bcabe13 --- /dev/null +++ b/tests/airos/test_intermediate.py @@ -0,0 +1,33 @@ +import unittest + +from netjsonconfig.backends.airos.intermediate import intermediate_to_list + + +class TestIntermediateConversion(unittest.TestCase): + + def test_dict_conversion(self): + i = [{'spam': {'eggs': 'spam and eggs'}}] + + o = [{'spam.eggs': 'spam and eggs'}] + + self.assertEqual(intermediate_to_list(i), o) + + def test_list_conversion(self): + i = [{'kings': [{'henry': 'the first'}, {'jacob': 'the second'}]}] + + o = [{'kings.1.henry': 'the first', 'kings.2.jacob': 'the second'}] + + self.assertEqual(intermediate_to_list(i), o) + + def test_multiple_conversion(self): + i = [ + {'snakes': {'loved': 'yes'}}, + {'dogs': {'loved': 'yes'}}, + ] + + o = [ + {'snakes.loved': 'yes'}, + {'dogs.loved': 'yes'}, + ] + + self.assertEqual(intermediate_to_list(i), o) diff --git a/tests/airos/test_iptables.py b/tests/airos/test_iptables.py new file mode 100644 index 000000000..9f4106052 --- /dev/null +++ b/tests/airos/test_iptables.py @@ -0,0 +1,41 @@ +from .mock import ConverterTest, IptablesAirOs + + +class IptablesConverter(ConverterTest): + backend = IptablesAirOs + + def test_bridge(self): + o = self.backend({ + 'netmode': 'bridge', + }) + o.to_intermediate() + expected = [ + { + 'status': 'disabled', + }, + { + 'sys.portfw.status': 'disabled', + 'sys.status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['iptables'], expected) + + def test_router(self): + o = self.backend({ + 'netmode': 'router', + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + 'sys.portfw.status': 'disabled', + 'sys.fw.status': 'disabled', + 'sys.mgmt.1.devname': 'br0', + 'sys.mgmt.1.status': 'enabled', + 'sys.mgmt.status': 'enabled', + 'sys.status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['iptables'], expected) diff --git a/tests/airos/test_netconf.py b/tests/airos/test_netconf.py new file mode 100644 index 000000000..d01a567fa --- /dev/null +++ b/tests/airos/test_netconf.py @@ -0,0 +1,530 @@ +from unittest import skip + +from .mock import ConverterTest, NetconfAirOs + + +class TestNetconfConverter(ConverterTest): + + backend = NetconfAirOs + + def test_netconf_key(self): + o = self.backend({ + 'interfaces': [] + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + def test_active_interface(self): + o = self.backend({ + 'interfaces': [{ + 'name': 'eth0', + 'type': 'ethernet', + }] + }) + + o.to_intermediate() + + expected = [ + { + '1.autoip.status': 'disabled', + '1.autoneg': 'disabled', + '1.devname': 'eth0', + '1.flowcontrol.tx.status': 'disabled', + '1.flowcontrol.rx.status': 'disabled', + '1.mtu': 1500, + '1.status': 'enabled', + '1.up': 'enabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + def test_inactive_interface(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'eth0', + 'type': 'ethernet', + 'disabled': True, + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.autoip.status': 'disabled', + '1.autoneg': 'disabled', + '1.devname': 'eth0', + '1.flowcontrol.tx.status': 'disabled', + '1.flowcontrol.rx.status': 'disabled', + '1.mtu': 1500, + '1.status': 'enabled', + '1.up': 'disabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + def test_vlan(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'eth0.1', + 'type': 'ethernet', + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.devname': 'eth0.1', + '1.mtu': 1500, + '1.status': 'enabled', + '1.up': 'enabled', + '1.autoip.status': 'disabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + def test_management_vlan(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'eth0.1', + 'type': 'ethernet', + 'addresses': [ + { + 'address': '192.168.1.20', + 'family': 'ipv4', + 'role': 'mlan', + 'mask': 24, + 'proto': 'static', + } + ] + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.devname': 'eth0.1', + '1.ip': '192.168.1.20', + '1.netmask': '255.255.255.0', + '1.mtu': 1500, + '1.role': 'mlan', + '1.status': 'enabled', + '1.up': 'enabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + def test_access_point(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'wlan0', + 'type': 'wireless', + 'wireless': { + 'radio': 'ath0', + 'mode': 'access_point', + 'ssid': 'ap-ssid-example', + } + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.devname': 'ath0', + '1.mtu': 1500, + '1.status': 'enabled', + '1.up': 'enabled', + '1.autoip.status': 'disabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + def test_station(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'wlan0', + 'type': 'wireless', + 'wireless': { + 'radio': 'ath0', + 'mode': 'station', + 'bssid': '00:11:22:33:44:55', + 'ssid': 'ap-ssid-example', + } + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.devname': 'ath0', + '1.status': 'enabled', + '1.up': 'enabled', + '1.mtu': 1500, + '1.autoip.status': 'disabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + @skip("Airos does not support ``adhoc`` mode") + def test_adhoc(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'wlan0', + 'type': 'wireless', + 'wireless': { + 'radio': 'ath0', + 'mode': 'adhoc', + 'ssid': 'ap-ssid-example', + } + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.devname': 'ath0', + '1.status': 'enabled', + '1.up': 'enabled', + '1.mtu': 1500, + '1.autoip.status': 'disabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + @skip("Airos does not support wds") + def test_wds(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'wlan0', + 'type': 'wireless', + 'wireless': { + 'radio': 'ath0', + 'mode': 'wds', + 'ssid': 'ap-ssid-example', + } + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.devname': 'ath0', + '1.mtu': 1500, + '1.up': 'enabled', + '1.status': 'enabled', + '1.autoip.status': 'disabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + @skip("Airos does not support ``monitor`` mode") + def test_monitor(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'wlan0', + 'type': 'wireless', + 'wireless': { + 'radio': 'ath0', + 'mode': 'monitor', + 'ssid': 'ap-ssid-example', + } + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.devname': 'ath0', + '1.status': 'enabled', + '1.up': 'enabled', + '1.autoip.status': 'enabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + @skip("AirOS does not support 802.11s") + def test_80211s(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'wlan0', + 'type': 'wireless', + 'wireless': { + 'radio': 'ath0', + 'mode': 'access_point', + 'protocol': '802.11s', + 'ssid': 'ap-ssid-example', + } + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.devname': 'ath0', + '1.status': 'enabled', + '1.up': 'enabled', + '1.autoip.status': 'disabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + def test_bridge(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'br0', + 'type': 'bridge', + 'bridge_members': [ + 'eth0', + 'eth1', + ], + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.devname': 'br0', + '1.status': 'enabled', + '1.up': 'enabled', + '1.mtu': 1500, + '1.autoip.status': 'disabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + @skip("Airos does not support virtual interfaces") + def test_virtual(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'veth0', + 'type': 'virtual', + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.devname': 'veth0', + '1.status': 'enabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + @skip("Airos does not support ``loopback`` interface") + def test_loopback(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'lopp0', + 'type': 'loopback', + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.devname': 'loop0', + '1.status': 'enabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + @skip("Airos does not support ``other`` interfaces") + def test_other(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'fancyname0', + 'type': 'other', + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.devname': 'fancyname0', + '1.status': 'enabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + def test_dhcp(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'eth0', + 'type': 'ethernet', + 'addresses': [ + { + 'proto': 'dhcp', + 'family': 'ipv4', + }, + ] + }, + ], + }) + o.to_intermediate() + expected = [ + { + '1.autoip.status': 'enabled', + '1.autoneg': 'disabled', + '1.devname': 'eth0', + '1.flowcontrol.rx.status': 'disabled', + '1.flowcontrol.tx.status': 'disabled', + '1.mtu': 1500, + '1.status': 'enabled', + '1.up': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['netconf'], expected) + + def test_more_interfaces(self): + o = self.backend({ + 'interfaces': [ + { + 'name': 'eth0', + 'type': 'ethernet', + }, + { + 'name': 'wlan0', + 'type': 'wireless', + 'wireless': { + 'radio': 'ath0', + 'mode': 'station', + 'bssid': '00:11:22:33:44:55', + 'ssid': 'ap-ssid-example', + } + }, + { + 'name': 'br0', + 'type': 'bridge', + 'bridge_members': [ + 'eth0', + 'wlan0', + ], + }, + { + 'name': 'veth0', + 'type': 'virtual', + }, + { + 'name': 'loop0', + 'type': 'loopback', + } + ], + }) + o.to_intermediate() + expected = [ + { + '1.autoip.status': 'disabled', + '1.autoneg': 'disabled', + '1.devname': 'eth0', + '1.flowcontrol.rx.status': 'disabled', + '1.flowcontrol.tx.status': 'disabled', + '1.mtu': 1500, + '1.status': 'enabled', + '1.up': 'enabled', + }, + { + '2.autoip.status': 'disabled', + '2.devname': 'ath0', + '2.mtu': 1500, + '2.status': 'enabled', + '2.up': 'enabled', + }, + { + '3.autoip.status': 'disabled', + '3.devname': 'br0', + '3.mtu': 1500, + '3.status': 'enabled', + '3.up': 'enabled', + }, + { + '4.autoip.status': 'disabled', + '4.devname': 'veth0', + '4.mtu': 1500, + '4.status': 'enabled', + '4.up': 'enabled', + }, + { + '5.autoip.status': 'disabled', + '5.devname': 'loop0', + '5.mtu': 1500, + '5.status': 'enabled', + '5.up': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netconf'], expected) diff --git a/tests/airos/test_netmode.py b/tests/airos/test_netmode.py new file mode 100644 index 000000000..1790e101c --- /dev/null +++ b/tests/airos/test_netmode.py @@ -0,0 +1,44 @@ +from .mock import ConverterTest, NetmodeAirOs + + +class TestNetmodeConverter(ConverterTest): + + backend = NetmodeAirOs + + def test_netconf_key(self): + o = self.backend({ + }) + o.to_intermediate() + expected = [ + { + 'status': 'bridge', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netmode'], expected) + + def test_bridge(self): + o = self.backend({ + 'netmode': 'bridge', + }) + o.to_intermediate() + expected = [ + { + 'status': 'bridge', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netmode'], expected) + + def test_router(self): + o = self.backend({ + 'netmode': 'router', + }) + o.to_intermediate() + expected = [ + { + 'status': 'router', + }, + ] + + self.assertEqualConfig(o.intermediate_data['netmode'], expected) diff --git a/tests/airos/test_ntp.py b/tests/airos/test_ntp.py new file mode 100644 index 000000000..6ae41a710 --- /dev/null +++ b/tests/airos/test_ntp.py @@ -0,0 +1,114 @@ +from .mock import ConverterTest, NtpclientAirOs + + +class TestResolvConverter(ConverterTest): + + backend = NtpclientAirOs + + def test_ntp_key(self): + o = self.backend({ + 'ntp': {} + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + '1.server': '0.pool.ntp.org', + '1.status': 'enabled', + }, + { + '2.server': '1.pool.ntp.org', + '2.status': 'enabled', + }, + { + '3.server': '2.pool.ntp.org', + '3.status': 'enabled', + }, + { + '4.server': '3.pool.ntp.org', + '4.status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['ntpclient'], expected) + + def test_no_ntp_server(self): + o = self.backend({ + 'ntp': { + 'enabled': False, + } + }) + o.to_intermediate() + expected = [ + { + 'status': 'disabled', + }, + { + '1.server': '0.pool.ntp.org', + '1.status': 'enabled', + }, + { + '2.server': '1.pool.ntp.org', + '2.status': 'enabled', + }, + { + '3.server': '2.pool.ntp.org', + '3.status': 'enabled', + }, + { + '4.server': '3.pool.ntp.org', + '4.status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['ntpclient'], expected) + + def test_single_ntp_server(self): + o = self.backend({ + 'ntp': { + 'enabled': True, + 'server': [ + '0.openwrt.pool.ntp.org', + ] + }, + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + '1.server': '0.openwrt.pool.ntp.org', + '1.status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['ntpclient'], expected) + + def test_multiple_ntp_server(self): + o = self.backend({ + 'ntp': { + 'server': [ + '0.openwrt.pool.ntp.org', + '1.openwrt.pool.ntp.org', + ], + } + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + '1.server': '0.openwrt.pool.ntp.org', + '1.status': 'enabled', + }, + { + '2.server': '1.openwrt.pool.ntp.org', + '2.status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['ntpclient'], expected) diff --git a/tests/airos/test_pwdog.py b/tests/airos/test_pwdog.py new file mode 100644 index 000000000..30b5e2ad7 --- /dev/null +++ b/tests/airos/test_pwdog.py @@ -0,0 +1,22 @@ +from .mock import ConverterTest, PwdogAirOs + + +class TestPwdogConverter(ConverterTest): + + backend = PwdogAirOs + + def test_ntp_key(self): + o = self.backend({ + "general": {} + }) + o.to_intermediate() + expected = [ + { + 'delay': 300, + 'period': 300, + 'retry': 3, + 'status': 'disabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['pwdog'], expected) diff --git a/tests/airos/test_radio.py b/tests/airos/test_radio.py new file mode 100644 index 000000000..9456f89e6 --- /dev/null +++ b/tests/airos/test_radio.py @@ -0,0 +1,269 @@ +from unittest import skip + +from .mock import ConverterTest, RadioAirOs + + +class TestRadioStationConverter(ConverterTest): + """ + Test radio device configuration for ``station`` wireless lan + """ + + backend = RadioAirOs + + def test_no_radio(self): + o = self.backend({ + "radios": [] + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + 'countrycode': 380, + }, + ] + + self.assertEqualConfig(o.intermediate_data['radio'], expected) + + def test_active_radio(self): + o = self.backend({ + "interfaces": [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'radio': 'ath0', + 'mode': 'station', + 'ssid': 'ubnt', + 'bssid': '00:11:22:33:44:55', + 'encryption': { + 'protocol': 'none', + }, + }, + }, + ], + "radios": [ + { + 'name': 'ath0', + 'channel': 1, + 'channel_width': 20, + 'protocol': '802.11n', + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.ack.auto': 'enabled', + '1.ackdistance': 643, + '1.acktimeout': 35, + '1.ampdu.frames': 32, + '1.ampdu.status': 'enabled', + '1.antenna.gain': 3, + '1.antenna.id': 2, + '1.atpc.sta.status': 'enabled', + '1.atpc.status': 'disabled', + '1.atpc.threshold': 36, + '1.cable.loss': 0, + '1.center.1.freq': 0, + '1.chanbw': 0, + '1.cmsbias': 0, + '1.countrycode': 380, + '1.cwm.enable': 0, + '1.cwm.mode': 1, + '1.devname': 'ath0', + '1.dfs.status': 'enabled', + '1.freq': 0, + '1.ieee_mode': 'auto', + '1.low_txpower_mode': 'disabled', + '1.mode': 'managed', + '1.obey': 'enabled', + '1.polling': 'enabled', + '1.polling_11ac_11n_compat': 0, + '1.polling_ff_dl_ratio': 50, + '1.polling_ff_dur': 0, + '1.polling_ff_timing': 0, + '1.pollingnoack': 0, + '1.pollingpri': 2, + '1.ptpmode': 1, + '1.reg_obey': 'enabled', + '1.rx_sensitivity': -96, + '1.scan_list.status': 'disabled', + '1.scanbw.status': 'disabled', + '1.status': 'enabled', + '1.subsystemid': '0xe7f5', + '1.txpower': 24, + }, + { + 'countrycode': 380, + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['radio'], expected) + + @skip('channel width breaks the fast build script') + def test_channel_width(self): + """ + TODO: channel brandwidth tested only on 802.11ac + """ + o = self.backend({ + "interfaces": [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'radio': 'ath0', + 'mode': 'station', + 'ssid': 'ubnt', + 'bssid': '00:11:22:33:44:55', + 'encryption': { + 'protocol': 'none', + }, + }, + }, + ], + "radios": [ + { + 'name': 'ath0', + 'channel': 1, + 'channel_width': 80, + 'protocol': '802.11ac', + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.ack.auto': 'enabled', + '1.ackdistance': 643, + '1.acktimeout': 35, + '1.ampdu.frames': 32, + '1.ampdu.status': 'enabled', + '1.antenna.gain': 3, + '1.antenna.id': 2, + '1.atpc.sta.status': 'enabled', + '1.atpc.status': 'disabled', + '1.atpc.threshold': 36, + '1.cable.loss': 0, + '1.center.1.freq': 0, + '1.chanbw': 80, + '1.cmsbias': 0, + '1.countrycode': 380, + '1.cwm.enable': 0, + '1.cwm.mode': 1, + '1.devname': 'ath0', + '1.dfs.status': 'enabled', + '1.freq': 0, + '1.ieee_mode': 'auto', + '1.low_txpower_mode': 'disabled', + '1.mode': 'managed', + '1.obey': 'enabled', + '1.polling': 'enabled', + '1.polling_11ac_11n_compat': 0, + '1.polling_ff_dl_ratio': 50, + '1.polling_ff_dur': 0, + '1.polling_ff_timing': 0, + '1.pollingnoack': 0, + '1.pollingpri': 2, + '1.ptpmode': 1, + '1.reg_obey': 'enabled', + '1.rx_sensitivity': -96, + '1.scan_list.status': 'disabled', + '1.scanbw.status': 'disabled', + '1.status': 'enabled', + '1.subsystemid': '0xe7f5', + '1.txpower': 24, + }, + { + 'countrycode': 380, + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['radio'], expected) + + +class TestRadioAccessPointConverter(ConverterTest): + """ + Test radio device configuration for ``access_point`` wireless lan + """ + + backend = RadioAirOs + + def test_active_radio(self): + o = self.backend({ + "interfaces": [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'wireless': { + 'radio': 'ath0', + 'mode': 'access_point', + 'bssid': '00:11:22:33:44:55', + 'ssid': 'ubnt', + 'encryption': { + 'protocol': 'none', + }, + }, + }, + ], + "radios": [ + { + 'name': 'ath0', + 'channel': 36, + 'channel_width': 80, + 'protocol': '802.11ac', + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.ack.auto': 'enabled', + '1.ackdistance': 643, + '1.acktimeout': 35, + '1.ampdu.frames': 32, + '1.ampdu.status': 'enabled', + '1.antenna.gain': 3, + '1.antenna.id': 2, + '1.atpc.sta.status': 'enabled', + '1.atpc.status': 'disabled', + '1.atpc.threshold': 36, + '1.cable.loss': 0, + '1.center.1.freq': 0, + '1.chanbw': 80, + '1.cmsbias': 0, + '1.countrycode': 380, + '1.cwm.enable': 0, + '1.cwm.mode': 1, + '1.devname': 'ath0', + '1.dfs.status': 'enabled', + '1.freq': 0, + '1.ieee_mode': '11acvht80', + '1.low_txpower_mode': 'disabled', + '1.mode': 'master', + '1.obey': 'enabled', + '1.polling': 'enabled', + '1.polling_11ac_11n_compat': 0, + '1.polling_ff_dl_ratio': 50, + '1.polling_ff_dur': 0, + '1.polling_ff_timing': 0, + '1.pollingnoack': 0, + '1.pollingpri': 2, + '1.ptpmode': 1, + '1.reg_obey': 'enabled', + '1.rx_sensitivity': -96, + '1.scan_list.status': 'disabled', + '1.scanbw.status': 'disabled', + '1.status': 'enabled', + '1.subsystemid': '0xe7f5', + '1.txpower': 24, + }, + { + 'countrycode': 380, + 'status': 'enabled', + + }, + ] + + self.assertEqualConfig(o.intermediate_data['radio'], expected) diff --git a/tests/airos/test_resolv.py b/tests/airos/test_resolv.py new file mode 100644 index 000000000..9f3172c3a --- /dev/null +++ b/tests/airos/test_resolv.py @@ -0,0 +1,69 @@ +from .mock import ConverterTest, ResolvAirOs + + +class TestResolvConverter(ConverterTest): + + backend = ResolvAirOs + + def test_resolv(self): + o = self.backend({ + "dns_servers": [ + "10.150.42.1" + ], + }) + o.to_intermediate() + expected = [ + { + 'host.1.name': 'airos', + 'host.1.status': 'enabled', + }, + { + 'nameserver.1.ip': '10.150.42.1', + 'nameserver.1.status': 'enabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['resolv'], expected) + + def test_no_dns_server(self): + o = self.backend({ + "dns_servers": [], + }) + o.to_intermediate() + expected = [ + { + 'host.1.name': 'airos', + 'host.1.status': 'enabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['resolv'], expected) + + def test_dns_server(self): + o = self.backend({ + "dns_servers": [ + "192.168.1.1" + ], + }) + o.to_intermediate() + expected = [ + { + 'host.1.name': 'airos', + 'host.1.status': 'enabled', + }, + { + 'nameserver.1.ip': '192.168.1.1', + 'nameserver.1.status': 'enabled' + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['resolv'], expected) diff --git a/tests/airos/test_route.py b/tests/airos/test_route.py new file mode 100644 index 000000000..eab0ede3a --- /dev/null +++ b/tests/airos/test_route.py @@ -0,0 +1,62 @@ +from .mock import ConverterTest, RouteAirOs + + +class TestRouteConverter(ConverterTest): + + backend = RouteAirOs + + def test_gateway_interface(self): + o = self.backend({ + 'interfaces': [{ + 'name': 'eth0', + 'type': 'ethernet', + 'addresses': [ + { + 'family': 'ipv4', + 'proto': 'dhcp', + 'gateway': '192.168.0.1' + } + ] + }] + }) + + o.to_intermediate() + + expected = [ + { + '1.devname': 'eth0', + '1.gateway': '192.168.0.1', + '1.ip': '0.0.0.0', + '1.netmask': 0, + '1.status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['route'], expected) + + def test_user_route(self): + o = self.backend({ + 'routes': [ + { + 'cost': 0, + 'destination': '192.178.1.0/24', + 'device': 'br0', + 'next': '192.178.1.1', + 'source': '192.168.1.20', + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.ip': '192.178.1.0', + '1.netmask': '255.255.255.0', + '1.gateway': '192.178.1.1', + '1.status': 'enabled', + }, + { + 'status': 'enabled', + }, + ] + + self.assertEqualConfig(o.intermediate_data['route'], expected) diff --git a/tests/airos/test_snmp.py b/tests/airos/test_snmp.py new file mode 100644 index 000000000..090f5323b --- /dev/null +++ b/tests/airos/test_snmp.py @@ -0,0 +1,43 @@ +from .mock import ConverterTest, SnmpAirOs + + +class TestSnmpConverter(ConverterTest): + """ + tests for backends.airos.renderers.SystemRenderer + """ + backend = SnmpAirOs + + def test_defaults(self): + + o = self.backend({}) + o.to_intermediate() + expected = [ + { + 'community': 'public', + 'contact': '', + 'location': '', + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['snmp'], expected) + + def test_custom_info(self): + + o = self.backend({ + 'general': { + 'mantainer': 'noone@somedomain.com', + 'location': 'somewhere in the woods', + } + }) + o.to_intermediate() + expected = [ + { + 'community': 'public', + 'contact': 'noone@somedomain.com', + 'location': 'somewhere in the woods', + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['snmp'], expected) diff --git a/tests/airos/test_sshd.py b/tests/airos/test_sshd.py new file mode 100644 index 000000000..fc6fa6afd --- /dev/null +++ b/tests/airos/test_sshd.py @@ -0,0 +1,55 @@ +from .mock import ConverterTest, SshdAirOs + + +class TestSshdConverter(ConverterTest): + + backend = SshdAirOs + + def test_with_password(self): + o = self.backend({ + 'sshd': { + 'port': 22, + 'enabled': True, + 'password_auth': True, + }, + }) + o.to_intermediate() + expected = [ + { + 'auth.passwd': 'enabled', + 'port': 22, + 'status': 'enabled', + } + ] + self.assertEqualConfig(o.intermediate_data['sshd'], expected) + + def test_with_key(self): + o = self.backend({ + 'sshd': { + 'port': 22, + 'enabled': True, + 'password_auth': True, + 'keys': [ + { + 'type': 'ssh-rsa', + 'key': 'my-public-key-here', + 'comment': 'this is netjsonconfig pubkey', + 'enabled': True, + } + ] + }, + }) + o.to_intermediate() + expected = [ + { + 'auth.passwd': 'enabled', + 'auth.key.1.status': 'enabled', + 'auth.key.1.type': 'ssh-rsa', + 'auth.key.1.value': 'my-public-key-here', + 'auth.key.1.comment': 'this is netjsonconfig pubkey', + 'port': 22, + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['sshd'], expected) diff --git a/tests/airos/test_syslog.py b/tests/airos/test_syslog.py new file mode 100644 index 000000000..7b52a8dea --- /dev/null +++ b/tests/airos/test_syslog.py @@ -0,0 +1,17 @@ +from .mock import ConverterTest, SyslogAirOs + + +class TestSyslogConverter(ConverterTest): + backend = SyslogAirOs + + def test_active(self): + o = self.backend({}) + o.to_intermediate() + expected = [ + { + 'remote.port': 514, + 'remote.status': 'disabled', + 'status': 'enabled', + } + ] + self.assertEqualConfig(o.intermediate_data['syslog'], expected) diff --git a/tests/airos/test_system.py b/tests/airos/test_system.py new file mode 100644 index 000000000..c6c1d37c1 --- /dev/null +++ b/tests/airos/test_system.py @@ -0,0 +1,19 @@ +from .mock import ConverterTest, SystemAirOs + + +class TestSystemConverter(ConverterTest): + backend = SystemAirOs + + def test_active(self): + o = self.backend({}) + o.to_intermediate() + expected = [ + { + 'airosx.prov.status': 'enabled', + 'cfg.version': 0, + 'date.status': 'disabled', + 'external.reset': 'enabled', + 'timezone': 'GMT' + } + ] + self.assertEqualConfig(o.intermediate_data['system'], expected) diff --git a/tests/airos/test_telnetd.py b/tests/airos/test_telnetd.py new file mode 100644 index 000000000..5fa8ffb77 --- /dev/null +++ b/tests/airos/test_telnetd.py @@ -0,0 +1,16 @@ +from .mock import ConverterTest, TelnetdAirOs + + +class TestTelnetdConverter(ConverterTest): + backend = TelnetdAirOs + + def test_active(self): + o = self.backend({}) + o.to_intermediate() + expected = [ + { + 'port': 23, + 'status': 'disabled' + } + ] + self.assertEqualConfig(o.intermediate_data['telnetd'], expected) diff --git a/tests/airos/test_tshaper.py b/tests/airos/test_tshaper.py new file mode 100644 index 000000000..5308f0d29 --- /dev/null +++ b/tests/airos/test_tshaper.py @@ -0,0 +1,11 @@ +from .mock import ConverterTest, TshaperAirOs + + +class TestTshaperConverter(ConverterTest): + backend = TshaperAirOs + + def test_active(self): + o = self.backend({}) + o.to_intermediate() + expected = [{'status': 'disabled'}] + self.assertEqualConfig(o.intermediate_data['tshaper'], expected) diff --git a/tests/airos/test_unms.py b/tests/airos/test_unms.py new file mode 100644 index 000000000..6b9812311 --- /dev/null +++ b/tests/airos/test_unms.py @@ -0,0 +1,11 @@ +from .mock import ConverterTest, UnmsAirOs + + +class TestUnmsConverter(ConverterTest): + backend = UnmsAirOs + + def test_active(self): + o = self.backend({}) + o.to_intermediate() + expected = [{'status': 'disabled'}] + self.assertEqualConfig(o.intermediate_data['unms'], expected) diff --git a/tests/airos/test_update.py b/tests/airos/test_update.py new file mode 100644 index 000000000..903952e1d --- /dev/null +++ b/tests/airos/test_update.py @@ -0,0 +1,13 @@ +from .mock import ConverterTest, UpdateAirOs + + +class TestUpdateConverter(ConverterTest): + backend = UpdateAirOs + + def test_status(self): + o = self.backend({}) + o.to_intermediate() + expected = [ + {'check.status': 'enabled'} + ] + self.assertEqualConfig(o.intermediate_data['update'], expected) diff --git a/tests/airos/test_upnpd.py b/tests/airos/test_upnpd.py new file mode 100644 index 000000000..5f8e81c61 --- /dev/null +++ b/tests/airos/test_upnpd.py @@ -0,0 +1,29 @@ +from .mock import ConverterTest, UpnpdAirOs + + +class TestUpnpdConverter(ConverterTest): + """ + tests for backends.airos.renderers.SystemRenderer + """ + backend = UpnpdAirOs + + def test_bridge(self): + + o = self.backend({ + 'netmode': 'bridge', + }) + o.to_intermediate() + with self.assertRaises(KeyError): + o.intermediate_data['upnpd'] + + def test_router(self): + o = self.backend({ + 'netmode': 'router', + }) + o.to_intermediate() + expected = [ + { + 'status': 'disabled', + } + ] + self.assertEqualConfig(o.intermediate_data['upnpd'], expected) diff --git a/tests/airos/test_users.py b/tests/airos/test_users.py new file mode 100644 index 000000000..42af6e879 --- /dev/null +++ b/tests/airos/test_users.py @@ -0,0 +1,31 @@ +from .mock import ConverterTest, UsersAirOs + + +class TestUsersConverter(ConverterTest): + """ + tests for backends.airos.renderers.SystemRenderer + """ + backend = UsersAirOs + + def test_user(self): + + o = self.backend({ + 'user': { + 'name': 'ubnt', + 'password': 'changeme', + 'salt': 'goodsalt', + } + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + '1.name': 'ubnt', + '1.password': '$1$goodsalt$changeme', + '1.status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['users'], expected) diff --git a/tests/airos/test_vlan.py b/tests/airos/test_vlan.py new file mode 100644 index 000000000..4c6e51a57 --- /dev/null +++ b/tests/airos/test_vlan.py @@ -0,0 +1,174 @@ +from .mock import ConverterTest, VlanAirOs + + +class TestVlanConverter(ConverterTest): + """ + tests for backends.airos.renderers.SystemRenderer + """ + backend = VlanAirOs + + def test_active_vlan(self): + o = self.backend({ + "interfaces": [ + { + "type": "ethernet", + "name": "eth0.1", + "disabled": False, + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.comment': '', + '1.devname': 'eth0', + '1.id': '1', + '1.status': 'enabled', + }, + { + 'status': 'enabled', + } + ] + self.assertEqualConfig(o.intermediate_data['vlan'], expected) + + def test_disabled_vlan(self): + o = self.backend({ + "interfaces": [ + { + "type": "ethernet", + "name": "eth0.1", + "disabled": True, + } + ] + }) + expected = [ + { + '1.comment': '', + '1.devname': 'eth0', + '1.id': '1', + '1.status': 'disabled', + }, + { + 'status': 'enabled', + } + ] + o.to_intermediate() + self.assertEqualConfig(o.intermediate_data['vlan'], expected) + + def test_many_vlan(self): + o = self.backend({ + "interfaces": [ + { + "type": "ethernet", + "name": "eth0.1", + "disabled": False, + }, + { + "type": "ethernet", + "name": "eth0.2", + "disabled": False, + } + ] + }) + expected = [ + { + '1.comment': '', + '1.devname': 'eth0', + '1.id': '1', + '1.status': 'enabled', + }, + { + '2.comment': '', + '2.devname': 'eth0', + '2.id': '2', + '2.status': 'enabled', + }, + { + 'status': 'enabled', + } + ] + o.to_intermediate() + self.assertEqualConfig(o.intermediate_data['vlan'], expected) + + def test_mixed_vlan(self): + o = self.backend({ + "interfaces": [ + { + "type": "ethernet", + "name": "eth0.1", + "disabled": True, + }, + { + "type": "ethernet", + "name": "eth0.2", + "disabled": False, + } + ] + }) + expected = [ + { + '1.comment': '', + '1.devname': 'eth0', + '1.id': '1', + '1.status': 'disabled', + }, + { + '2.comment': '', + '2.devname': 'eth0', + '2.id': '2', + '2.status': 'enabled', + }, + { + 'status': 'enabled', + } + ] + o.to_intermediate() + self.assertEqualConfig(o.intermediate_data['vlan'], expected) + + def test_no_vlan(self): + o = self.backend({ + "interfaces": [ + { + "type": "ethernet", + "name": "eth0", + "disabled": True, + }, + ] + }) + expected = [ + { + 'status': 'enabled', + }, + ] + o.to_intermediate() + self.assertEqualConfig(o.intermediate_data['vlan'], expected) + + def test_one_vlan(self): + o = self.backend({ + "interfaces": [ + { + "type": "ethernet", + "name": "eth0", + "disabled": False, + }, + { + "type": "ethernet", + "name": "eth0.1", + "disabled": False, + }, + + ] + }) + expected = [ + { + '1.comment': '', + '1.devname': 'eth0', + '1.id': '1', + '1.status': 'enabled', + }, + { + 'status': 'enabled', + }, + ] + o.to_intermediate() + self.assertEqualConfig(o.intermediate_data['vlan'], expected) diff --git a/tests/airos/test_wireless.py b/tests/airos/test_wireless.py new file mode 100644 index 000000000..4c443853e --- /dev/null +++ b/tests/airos/test_wireless.py @@ -0,0 +1,243 @@ +from .mock import ConverterTest, WirelessAirOs + + +class TestWirelessAccessConverter(ConverterTest): + + backend = WirelessAirOs + + def test_active_wireless(self): + + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'mac': 'de:9f:db:30:c9:c5', + 'mtu': 1500, + 'txqueuelen': 1000, + 'autostart': True, + 'wireless': { + 'radio': 'radio0', + 'mode': 'access_point', + 'ssid': 'ap-ssid-example', + }, + 'addresses': [ + { + 'address': '192.168.1.1', + 'mask': 24, + 'family': 'ipv4', + 'proto': 'static', + } + ], + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.addmtikie': 'enabled', + '1.devname': 'radio0', + '1.hide_ssid': 'disabled', + '1.l2_isolation': 'disabled', + '1.mac_acl.policy': 'allow', + '1.mac_acl.status': 'disabled', + '1.mcast.enhance': 0, + '1.rate.auto': 'enabled', + '1.rate.mcs': -1, + '1.security.type': 'none', + '1.signal_led1': 75, + '1.signal_led2': 50, + '1.signal_led3': 25, + '1.signal_led4': 15, + '1.signal_led_status': 'enabled', + '1.ssid': 'ap-ssid-example', + '1.status': 'enabled', + '1.wds.status': 'enabled', + + }, + { + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['wireless'], expected) + + def test_inactive_wireless(self): + + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'mac': 'de:9f:db:30:c9:c5', + 'mtu': 1500, + 'txqueuelen': 1000, + 'autostart': True, + 'disabled': True, + 'wireless': { + 'radio': 'radio0', + 'mode': 'access_point', + 'ssid': 'ap-ssid-example', + }, + 'addresses': [ + { + 'address': '192.168.1.1', + 'mask': 24, + 'family': 'ipv4', + 'proto': 'static', + } + ], + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.addmtikie': 'enabled', + '1.devname': 'radio0', + '1.hide_ssid': 'disabled', + '1.l2_isolation': 'disabled', + '1.mac_acl.policy': 'allow', + '1.mac_acl.status': 'disabled', + '1.mcast.enhance': 0, + '1.rate.auto': 'enabled', + '1.rate.mcs': -1, + '1.security.type': 'none', + '1.signal_led1': 75, + '1.signal_led2': 50, + '1.signal_led3': 25, + '1.signal_led4': 15, + '1.signal_led_status': 'enabled', + '1.ssid': 'ap-ssid-example', + '1.status': 'disabled', + '1.wds.status': 'enabled', + }, + { + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['wireless'], expected) + + +class TestWirelessStationConverter(ConverterTest): + + backend = WirelessAirOs + + def test_active_wireless(self): + + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'mac': 'de:9f:db:30:c9:c5', + 'mtu': 1500, + 'txqueuelen': 1000, + 'autostart': True, + 'wireless': { + 'radio': 'radio0', + 'mode': 'station', + 'ssid': 'ap-ssid-example', + 'bssid': '00:11:22:33:44:55' + }, + 'addresses': [ + { + 'address': '192.168.1.1', + 'mask': 24, + 'family': 'ipv4', + 'proto': 'static', + } + ], + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.addmtikie': 'enabled', + '1.ap': '00:11:22:33:44:55', + '1.devname': 'radio0', + '1.hide_ssid': 'disabled', + '1.l2_isolation': 'disabled', + '1.mac_acl.policy': 'allow', + '1.mac_acl.status': 'disabled', + '1.mcast.enhance': 0, + '1.rate.auto': 'enabled', + '1.rate.mcs': -1, + '1.security.type': 'none', + '1.signal_led1': 75, + '1.signal_led2': 50, + '1.signal_led3': 25, + '1.signal_led4': 15, + '1.signal_led_status': 'enabled', + '1.ssid': 'ap-ssid-example', + '1.status': 'enabled', + '1.wds.status': 'enabled', + + }, + { + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['wireless'], expected) + + def test_inactive_wireless(self): + + o = self.backend({ + 'interfaces': [ + { + 'type': 'wireless', + 'name': 'wlan0', + 'mac': 'de:9f:db:30:c9:c5', + 'mtu': 1500, + 'txqueuelen': 1000, + 'autostart': True, + 'disabled': True, + 'wireless': { + 'radio': 'radio0', + 'mode': 'station', + 'ssid': 'ap-ssid-example', + 'bssid': '00:11:22:33:44:55', + }, + 'addresses': [ + { + 'address': '192.168.1.1', + 'mask': 24, + 'family': 'ipv4', + 'proto': 'static', + } + ], + } + ] + }) + o.to_intermediate() + expected = [ + { + '1.addmtikie': 'enabled', + '1.ap': '00:11:22:33:44:55', + '1.devname': 'radio0', + '1.hide_ssid': 'disabled', + '1.l2_isolation': 'disabled', + '1.mac_acl.policy': 'allow', + '1.mac_acl.status': 'disabled', + '1.mcast.enhance': 0, + '1.rate.auto': 'enabled', + '1.rate.mcs': -1, + '1.security.type': 'none', + '1.signal_led1': 75, + '1.signal_led2': 50, + '1.signal_led3': 25, + '1.signal_led4': 15, + '1.signal_led_status': 'enabled', + '1.ssid': 'ap-ssid-example', + '1.status': 'disabled', + '1.wds.status': 'enabled', + }, + { + 'status': 'enabled', + } + ] + + self.assertEqualConfig(o.intermediate_data['wireless'], expected) diff --git a/tests/airos/test_wpasupplicant.py b/tests/airos/test_wpasupplicant.py new file mode 100644 index 000000000..1adcc703f --- /dev/null +++ b/tests/airos/test_wpasupplicant.py @@ -0,0 +1,359 @@ +from unittest import skip + +from netjsonconfig.exceptions import ValidationError + +from .mock import ConverterTest, WpasupplicantAirOs + + +class TestWpasupplicantStation(ConverterTest): + """ + Test the wpasupplicant converter for a + device in ``station`` mode + """ + backend = WpasupplicantAirOs + + def test_invalid_encryption(self): + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "mac": "de:9f:db:30:c9:c5", + "mtu": 1500, + "txqueuelen": 1000, + "autostart": True, + "wireless": { + "radio": "radio0", + "mode": "station", + "ssid": "ap-ssid-example", + "encryption": {"protocol": "wep"}, + }, + } + ] + }) + with self.assertRaises(ValidationError): + o.validate() + + def test_no_encryption(self): + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "mac": "de:9f:db:30:c9:c5", + "mtu": 1500, + "txqueuelen": 1000, + "autostart": True, + "wireless": { + "radio": "radio0", + "mode": "station", + "ssid": "ap-ssid-example", + "bssid": "00:11:22:33:44:55", + "encryption": {"protocol": "none"}, + }, + } + ] + }) + o.to_intermediate() + expected = [ + { + 'device.1.profile': 'AUTO', + 'device.1.status': 'enabled', + 'profile.1.name': 'AUTO', + 'profile.1.network.1.ssid': 'ap-ssid-example', + 'profile.1.network.1.priority': 100, + 'profile.1.network.1.key_mgmt.1.name': 'NONE', + 'profile.1.network.2.key_mgmt.1.name': 'NONE', + 'profile.1.network.2.priority': 2, + 'profile.1.network.2.status': 'disabled', + }, + { + 'status': 'enabled', + } + ] + self.assertEqualConfig(o.intermediate_data['wpasupplicant'], expected) + + def test_wpa2_personal(self): + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "mac": "de:9f:db:30:c9:c5", + "mtu": 1500, + "txqueuelen": 1000, + "autostart": True, + "wireless": { + "radio": "radio0", + "mode": "station", + "ssid": "ap-ssid-example", + "bssid": "00:11:22:33:44:55", + "encryption": { + "protocol": "wpa2_personal", + "key": "cucumber", + }, + }, + } + ] + }) + o.to_intermediate() + expected = [ + { + 'device.1.devname': 'radio0', + 'device.1.driver': 'madwifi', + 'device.1.profile': 'AUTO', + 'device.1.status': 'enabled', + 'profile.1.name': 'AUTO', + 'profile.1.network.1.eap.1.status': 'disabled', + 'profile.1.network.1.key_mgmt.1.name': 'WPA-PSK', + 'profile.1.network.1.pairwise.1.name': 'CCMP', + 'profile.1.network.1.phase2=auth': 'MSCHAPV2', + 'profile.1.network.1.priority': 100, + 'profile.1.network.1.proto.1.name': 'RSN', + 'profile.1.network.1.psk': 'cucumber', + 'profile.1.network.1.ssid': 'ap-ssid-example', + 'profile.1.network.2.key_mgmt.1.name': 'NONE', + 'profile.1.network.2.priority': 2, + 'profile.1.network.2.status': 'disabled', + }, + { + 'status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['wpasupplicant'], expected) + + def test_eap_wpa2_enterprise(self): + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "mac": "de:9f:db:30:c9:c5", + "mtu": 1500, + "txqueuelen": 1000, + "wireless": { + "radio": "radio0", + "mode": "station", + "ssid": "ap-ssid-example", + "bssid": "00:11:22:33:44:55", + "encryption": { + "protocol": "wpa2_enterprise", + "eap_type": "tls", + "identity": "definitely-fake-identity", + "password": "password1234", + }, + }, + } + ] + }) + o.to_intermediate() + expected = [ + { + 'device.1.devname': 'radio0', + 'device.1.driver': 'madwifi', + 'device.1.profile': 'AUTO', + 'device.1.status': 'enabled', + 'profile.1.name': 'AUTO', + 'profile.1.network.1.bssid': '00:11:22:33:44:55', + 'profile.1.network.1.eap.1.name': 'TTLS', + 'profile.1.network.1.eap.1.status': 'enabled', + 'profile.1.network.1.identity': 'definitely-fake-identity', + 'profile.1.network.1.key_mgmt.1.name': 'WPA-EAP', + 'profile.1.network.1.pairwise.1.name': 'CCMP', + 'profile.1.network.1.password': 'password1234', + 'profile.1.network.1.phase2=auth': 'MSCHAPV2', + 'profile.1.network.1.priority': 100, + 'profile.1.network.1.proto.1.name': 'RSN', + 'profile.1.network.1.ssid': 'ap-ssid-example', + 'profile.1.network.2.key_mgmt.1.name': 'NONE', + 'profile.1.network.2.priority': 2, + 'profile.1.network.2.status': 'disabled', + }, + { + 'status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['wpasupplicant'], expected) + + @skip("target later") + def test_peap_wpa2_enterprise(self): + + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "mac": "de:9f:db:30:c9:c5", + "mtu": 1500, + "txqueuelen": 1000, + "wireless": { + "radio": "radio0", + "mode": "station", + "ssid": "ap-ssid-example", + "encryption": { + "protocol": "wpa2_enterprise", + "server": "radius.example.com", + "key": "the-shared-key", + "acct_server": "accounting.example.com", + }, + }, + } + ] + }) + o.to_intermediate() + expected = [ + { + 'device.1.devname': 'radio0', + 'device.1.driver': 'madwifi', + 'device.1.profile': 'AUTO', + 'device.1.status': 'enabled', + 'profile.1.network.1.eap.1.name': 'PEAP', + 'profile.1.network.1.eap.1.status': 'enabled', + 'profile.1.network.1.identity': 'definitely-fake-identity', + 'profile.1.network.1.key_mgmt.1.name': 'WPA-EAP', + 'profile.1.network.1.pairwise.1.name': 'CCMP', + 'profile.1.network.1.password': 'password1234', + 'profile.1.network.1.phase2=auth': 'MSCHAPV2', + 'profile.1.network.1.priority': 100, + 'profile.1.network.1.proto.1.name': 'RSN', + 'profile.1.network.1.ssid': 'ap-ssid-example', + 'profile.1.network.2.key_mgmt.1.name': 'NONE', + 'profile.1.network.2.priority': 2, + 'profile.1.network.2.status': 'disabled', + }, + { + 'status': 'enabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['wpasupplicant'], expected) + + +class TestWpasupplicantAccess(ConverterTest): + """ + Test the wpasupplicant converter for a + device in ``access_point`` mode + """ + backend = WpasupplicantAirOs + + def test_no_encryption(self): + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "mac": "de:9f:db:30:c9:c5", + "mtu": 1500, + "txqueuelen": 1000, + "autostart": True, + "wireless": { + "radio": "radio0", + "mode": "access_point", + "ssid": "ap-ssid-example", + "encryption": {"protocol": "none"}, + }, + }, + ] + }) + o.to_intermediate() + expected = [ + { + 'status': 'enabled', + }, + { + 'device.1.profile': 'AUTO', + 'device.1.status': 'enabled', + 'profile.1.name': 'AUTO', + 'profile.1.network.1.key_mgmt.1.name': 'NONE', + 'profile.1.network.1.priority': 100, + 'profile.1.network.1.ssid': 'ap-ssid-example', + 'profile.1.network.2.key_mgmt.1.name': 'NONE', + 'profile.1.network.2.priority': 2, + 'profile.1.network.2.status': 'disabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['wpasupplicant'], expected) + + def test_wpa2_personal(self): + + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "mac": "de:9f:db:30:c9:c5", + "mtu": 1500, + "txqueuelen": 1000, + "autostart": True, + "wireless": { + "radio": "radio0", + "mode": "access_point", + "ssid": "ap-ssid-example", + "encryption": { + "protocol": "wpa2_personal", + "key": "cucumber", + }, + }, + } + ] + }) + o.to_intermediate() + expected = [ + { + 'status': 'disabled', + }, + { + 'device.1.profile': 'AUTO', + 'device.1.status': 'disabled', + 'profile.1.name': 'AUTO', + 'profile.1.network.1.key_mgmt.1.name': 'NONE', + 'profile.1.network.1.priority': 100, + 'profile.1.network.1.psk': 'cucumber', + 'profile.1.network.1.ssid': 'ap-ssid-example', + 'profile.1.network.2.key_mgmt.1.name': 'NONE', + 'profile.1.network.2.priority': 2, + 'profile.1.network.2.status': 'disabled', + } + ] + self.assertEqualConfig(o.intermediate_data['wpasupplicant'], expected) + + def test_eap_wpa2_enterprise(self): + o = self.backend({ + "interfaces": [ + { + "type": "wireless", + "name": "wlan0", + "mac": "de:9f:db:30:c9:c5", + "mtu": 1500, + "txqueuelen": 1000, + "wireless": { + "radio": "radio0", + "mode": "access_point", + "ssid": "ap-ssid-example", + "encryption": { + "protocol": "wpa2_enterprise", + "server": "radius.example.com", + "key": "the-shared-key", + "acct_server": "accounting.example.com", + }, + }, + } + ] + }) + o.to_intermediate() + expected = [ + { + 'status': 'disabled', + }, + { + 'device.1.profile': 'AUTO', + 'device.1.status': 'disabled', + 'profile.1.name': 'AUTO', + 'profile.1.network.1.key_mgmt.1.name': 'NONE', + 'profile.1.network.1.priority': 100, + 'profile.1.network.1.ssid': 'ap-ssid-example', + 'profile.1.network.2.key_mgmt.1.name': 'NONE', + 'profile.1.network.2.priority': 2, + 'profile.1.network.2.status': 'disabled', + }, + ] + self.assertEqualConfig(o.intermediate_data['wpasupplicant'], expected)