Ansible orcherstration for Openwrt - with Lua!
Ansible is build around a collection of modules that get send to the remote host to execute different tasks or collect information. Those modules are implemented in python. However on embedded systems such as routers, resources, in particular flash memory are scarce and a python runtime often not available.
Those modules communicate with the ansible-toolsuite via well defined interfaces and are executed via ssh. As each module is a standalone program, there is no dependency whatsoever on the implementation language. There are existing attempts like this which already implement a small set of modules as bash-scripts.
However the primary author of this project disagrees with some of the implementation decisions (e.g. sourcing files with key=value-pairs as a kind of parsing) and is generally a fan of (even rather limited in luas case) typing. So this project was born.
As the OpenWrt community seems to have a strange affection for lua, this repository currently implements the following modules:
- copy
- file
- lineinfile
- opkg
- stat
- ubus
- uci
Copy, file, lineinfile, stat and opkg are mostly straightforward ports of the official python modules included in the Ansible v2.1.1.0 release. However, there were some simplifications made:
- selinux file attributes are not supported
- validation commands are not supported
- file-operations are not guaranteed to be atomic
- permissions can only be specified in octal mode
- check_mode is only partly implemented
Apart from that, the modules should behave exactly like the upstream modules, making it possible to use local actions such as "template" which are built upon those modules.
For building the modules, perl and the Data::Compare library are required.
If you want to use the file related modules (copy, file, lineinfile, stat), the following opkg packages are required, which are not part of the standard images:
- luaposix
- coreutils-sha1sum
However, as the opkg-module is independent from those packages, you can install them in your playbook like this:
- name: Installing dependencies for file-related modules
opkg: pkg=luaposix,coreutils-sha1sum state=present update_cache=yes
Ansible currently has no notion of libraries used within modules (only limited support for ansibles own core python libraries is available). For more information please see this issue. Therefore all modules that should be used have to be fatpacked (that is, the module all referenced libraries have to be packed into one giant lua script). This is done by the fatpack.pl script. Usage is like this:
./src/fatpack.pl --input <module>.lua --output ./library/ --whitelist
io,os,posix.,ubus --truncate
To make this process easier, a Makefile is provided that packs all modules in
./src/
and places the fatpacked variants in library
for you. Just run make
in the projects top directory.
Please note, that this project is currently in alpha state. I used it to manage
my personal router (playbook coming soon), but it still might easily lock you
out of your device, eat your hamsters or worse. So please check your playbook
beforehand against a VM (e.g. the one from the openwrt-vagrant project which
can be built from the submodule in ./test/
) or be sure that your router has a
convenient reset/failsafe path.
Apart form the ./library/
folder, you might want to copy the provided ansible.cfg
as it configures ansible for better interoperability with the dropbear ssh-daemon used by openwrt.
For the following modules, please refer to the upstream documentation
As a replacement for then official setup module, information on the openwrt system can be gatherd via the ubus interface and will automatically be integrated into the host_facts for reuse in the playbook like this:
ubus: cmd=facts
Otherwise, this module is a slim wrapper around the ubus rpc-bus.
For a list of available ubus-service-providers and their functions, you can issue a list call. Please note that this call is not really useful in an automated setting:
$ ansible openwrt -i hosts -m ubus -a 'cmd=list'
openwrt | SUCCESS => {
"changed": false,
"invocations": {
"module_args": {
"command": "list"
}
},
"msg": "Gathered local signatures",
"signatures": {
[...]
"uci": {
[...]
"get": {
"config": 3,
"match": 2,
"option": 3,
"section": 3,
"type": 3,
"ubus_rpc_session": 3
},
[...]
},
[...]
}
}
Those signatures can then be used to make Calls via ubus:
ubus: cmd=call path=uci method=get message='{"config":"uhttpd", "section":"main", "option":"listen_http"}"'
As you can see, the ubus_rpc_session
parameter is automatically inserted for
you by the module. The ubus return value is returned in the result
field of the returned object and can be accessed like this:
- name: Query http listen ports
ubus: cmd=call path=uci method=get message='{"config":"uhttpd", "section":"main", "option":"listen_http"}"'
register: foo
- name: Do something
baz: param={{ result.value }}
As most ubus calls will most likely target the uci-system a dedicated module/ubus-wrapper for the uci configuration is provided. Basic familiarity with uci is assumed, so please refer to the upstream documentation otherwise. Most of the options should map quite naturally to the module parameters:
A special warning about types: UCI has two types for values internally: list
and option
. The module tries to infer the type by looking for ,
in the
input. If you need to force a singleentry list, please be sure to set the
forcelist=yes
parameter.
parameter | required | default | choices | comments |
---|---|---|---|---|
name | no | Path to the property to change. Syntax is config.section.option . Aliases: path, key |
||
value | no | For set: value to set the property to | ||
match | no | When present in a set or get op: properties a section must have to be modified or returned | ||
values | no | For set with match: values to set on matching section | ||
forcelist | no | false | Boolean | The module trys to guess the uci config type (list or string) from the supplied value via the existance of , in the input. Single entry lists require forcelist=yes to be recognized correctly |
state | no | present | present, absent, set, unset | State of the property |
op | no | configs, commit, revert, get | If specified, instead of enforcing a value, either list the available configurations, execute a commit/revert operation, or query properties. | |
reload | no | Boolean | Whether to reload the configuration from disk before executing. Aliases: reload_configs, reload-configs | |
autocomit | no | true | Boolean | Whether to automatically commit the changes made |
type | no | When creating a new section, a configuration-type is required. Can also be used to qualify a get. Aliases: section-type | ||
socket | no | Set a nonstandard path to the ubus socket if necessary | ||
timeout | no | Change the default ubus timeout |
Examples:
# Set a value
uci: name="system.@system[0].hostname" value="mysuperduperrouter"
# Delete a value
uci: name="system.@system[0].hostname" state=absent
# Revert and commit globally
uci: op=revert
uci: op=commit
# Only commit/revert a single section
uci: path=dropbear op=revert
uci: path=dropbear op=commit
# Create the uhttpd.test section with type uhttp
# and set foo=bar
uci: name=uhttpd.test.foo value=bar type="uhttpd" autocommit=false'
# Remove the uttpd.test section
uci: name=uhttpd.test state="absent" autocommit=true'
# Get a list of all available configuration files
uci: op=configs
An more complex example showing the usage of forcelist:
- name: Securing uhttpd - Disable listening on wan
uci: name={{ item.key }} value={{ uci.state.network.lan.ipaddr }}:{{ item.port }} forcelist=true autocommit=false
with_items:
- { key: 'uhttpd.main.listen_http', port: '80' }
- { key: 'uhttpd.main.listen_https', port: '443' }
notify:
- uci commit
Give me all your pullrequests :) If you find a bug in one of the provided modules
(quite possible) or want to contribute a new module, feel free to propose a
pullrequest.
To make development of the modules easier, two libraries are provided. The
ansible library in ./src/ansible.lua
tries to provide a easy starting point
for module development similar to ansibles ansible.module_utils.basic
library.
It will handle argument parsing for you:
local module = Ansible.new({
name = { aliases = {"pkg"}, required=true , type='list'},
state = { default = "present", choices={"present", "installed", "absent", "removed"} },
force = { default = "", choices={"", "depends", "maintainer", "reinstall", "overwrite", "downgrade", "space", "postinstall", "remove", "checksum", "removal-of-dependent-packages"} } ,
update_cache = { default = "no", aliases={ "update-cache" }, type='bool' }
})
module:parse(arg[1])
local p = module:get_params()
And provides some convenience function such as get_bin_path
, run_command
,
fail_json
and exit_json
. Currently, those are badly underdocumented, but
the names are mostly selfexplanatory, so just look through the functions in the
file.
local opkg_path = module:get_bin_path('echo', true, {'/bin'})
local rc, out, err = module:run_command(string.format("%s foobar", opkg_path))
if rc ~= 0 then
module:fail_json({msg="failed to echo foobar", info={rc=rc, out=out, err=err}})
else
module:exit_json({msg="successfully echod foobar", changed=false})
end
Additionally, the ./src/fileutils.lua
module has various wrappers for various
filesystemrelated tasks. Again: Please look up the functions in the sourcefile
and look how they are used in the provided modules.
The libraries and submodules were only included in this repository for convenience and are available under their own respective licenses:
- dkjson MIT License
- BinDecHex MIT License
- openwrt-in-vagrant MIT License
All other code is available under the terms and conditions of the AGPL3 license. For more details please see the LICENSE file.
In Orson Scott Cards marvellous Ender's Game series the term "ansible" refers to a device for faster than light communication. The philote is the (fictional) subatomic particle which delivers the actual messages.