Before starting, let's quickly introduce the main concepts used in netjsonconfig:
- configuration dictionary: python dictionary representing the configuration of a router
- backend: python class used to process the configuration and generate the final router configuration
- schema: each backend has a JSON-Schema which defines the useful configuration options that the backend is able to process
- validation: the configuration is validated against its JSON-Schema before being processed by the backend
- template: common configuration options shared among routers (eg: VPNs, SSID) which can be passed to backends
- context: variables that can be referenced from the configuration dictionary
Netjsonconfig is an implementation of the NetJSON format,
more specifically the DeviceConfiguration
object, therefore to understand the
configuration format that the library uses to generate the final router configurations
it is essential to read at least the relevant DeviceConfiguration section in the
NetJSON RFC.
Here it is a simple NetJSON DeviceConfiguration object:
{
"type": "DeviceConfiguration",
"general": {
"hostname": "RouterA"
},
"interfaces": [
{
"name": "eth0",
"type": "ethernet",
"addresses": [
{
"address": "192.168.1.1",
"mask": 24,
"proto": "static",
"family": "ipv4"
}
]
}
]
}
The previous example describes a device named RouterA
which has a single
network interface named eth0
with a statically assigned ip address 192.168.1.1/24
(CIDR notation).
Because netjsonconfig deals only with DeviceConfiguration
objects, the type
attribute can be omitted, the library will add the correct type automatically.
The previous configuration object therefore can be shortened to:
{
"general": {
"hostname": "RouterA"
},
"interfaces": [
{
"name": "eth0",
"type": "ethernet",
"addresses": [
{
"address": "192.168.1.1",
"mask": 24,
"proto": "static",
"family": "ipv4"
}
]
}
]
}
From now on we will use the term configuration dictionary to refer to NetJSON DeviceConfiguration objects.
A backend is a python class used to process the configuration dictionary and generate the final router configuration, each supported firmware or opearting system will have its own backend and third parties can write their own custom backends.
The current implemented backends are:
- :doc:`OpenWrt </backends/openwrt>`
- :doc:`OpenWisp </backends/openwisp>` (based on the
OpenWrt
backend)
Example initialization of OpenWrt
backend:
from netjsonconfig import OpenWrt
ipv6_router = OpenWrt({
"type": "DeviceConfiguration",
"interfaces": [
{
"name": "eth0.1",
"type": "ethernet",
"addresses": [
{
"address": "fd87::1",
"mask": 128,
"proto": "static",
"family": "ipv6"
}
]
}
]
})
Each backend has a JSON-Schema, all the backends have a schema which is derived
from the same parent schema, defined in netjsonconfig.backends.schema
(view source).
Since different backends may support different features each backend may extend its schema by adding custom definitions.
All the backends have a validate
method which is called automatically before
trying to process the configuration.
If the passed configuration violates the schema the validate
method will raise
a ValidationError
.
An instance of validation error has two public attributes:
message
: a human readable message explaining the errordetails
: a reference to the instance ofjsonschema.exceptions.ValidationError
which contains more details about what has gone wrong; for a complete reference see the python-jsonschema documentation
You may call the validate
method in your application arbitrarily, eg: before
trying to save the configuration dictionary into a database.
If you have devices with very similar configuration dictionaries you can store the shared blocks in one or more reusable templates which will be used as a base to build the final configuration.
Let's illustrate this with a practical example, we have two devices:
- Router1
- Router2
Both devices have an eth0
interface in DHCP mode; Router2 additionally has
an eth1
interface with a statically assigned ipv4 address.
The two routers can be represented with the following code:
from netjsonconfig import OpenWrt
router1 = OpenWrt({
"general": {"hostname": "Router1"}
"interfaces": [
{
"name": "eth0",
"type": "ethernet",
"addresses": [
{
"proto": "dhcp",
"family": "ipv4"
}
]
}
]
})
router2 = OpenWrt({
"general": {"hostname": "Router2"},
"interfaces": [
{
"name": "eth0",
"type": "ethernet",
"addresses": [
{
"proto": "dhcp",
"family": "ipv4"
}
]
},
{
"name": "eth1",
"type": "ethernet",
"addresses": [
{
"address": "192.168.1.1",
"mask": 24,
"proto": "static",
"family": "ipv4"
}
]
}
]
})
The two configuration dictionaries share the same settings for the eth0
interface, therefore we can make the eth0
settings our template and
refactor the previous code as follows:
from netjsonconfig import OpenWrt
dhcp_template = {
"interfaces": [
{
"name": "eth0",
"type": "ethernet",
"addresses": [
{
"proto": "dhcp",
"family": "ipv4"
}
]
}
]
}
router1 = OpenWrt(config={"general": {"hostname": "Router1"}},
templates=[dhcp_template])
router2_config = {
"general": {"hostname": "Router2"},
"interfaces": [
{
"name": "eth1",
"type": "ethernet",
"addresses": [
{
"address": "192.168.1.1",
"mask": 24,
"proto": "static",
"family": "ipv4"
}
]
}
]
}
router2 = OpenWrt(router2_config, templates=[dhcp_template])
The function used under the hood to merge dictionaries and lists
is netjsonconfig.utils.merge_config
:
.. autofunction:: netjsonconfig.utils.merge_config
You might have noticed that the templates
argument is a list; that's because
it's possible to pass multiple templates that will be added one on top of the
other to build the resulting configuration dictionary, allowing to reduce or
even eliminate repetitions.
Without variables, many bits of configuration cannot be stored in templates, because some parameters are unique to the device, think about things like a UUID or a public ip address.
With this feature it is possible to reference variables in the configuration dictionary, these variables will be evaluated when the configuration is rendered/generated.
Here's an example from the real world:
from netjsonconfig import OpenWrt
openwisp_config_template = {
"openwisp": [
{
"config_name": "controller",
"config_value": "http",
"url": "http://controller.examplewifiservice.com",
"interval": "60",
"verify_ssl": "1",
"uuid": "${UUID}",
"key": "${KEY}"
}
]
}
context = {
'UUID': '9d9032b2-da18-4d47-a414-1f7f605479e6',
'KEY': 'xk7OzA1qN6h1Ggxy8UH5NI8kQnbuLxsE'
}
router1 = OpenWrt(config={"general": {"hostname": "Router1"}},
templates=[openwisp_config_template],
context=context)
Let's see the result with:
>>> print(router1.render())
package system
config system
option hostname 'Router1'
option timezone 'UTC'
package openwisp
config controller 'http'
option interval '60'
option key 'xk7OzA1qN6h1Ggxy8UH5NI8kQnbuLxsE'
option url 'http://controller.examplewifiservice.com'
option uuid '9d9032b2-da18-4d47-a414-1f7f605479e6'
option verify_ssl '1'
If you are interested in this topic you can read more about the :doc:`Goals and Motivations <./goals>` of this project.
This software is licensed under the terms of the GPLv3 license, for more information, please see full LICENSE file.