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)