Improve/fix RFC3442 entries and add tests
- Generate the rfc3442 words using a filter plugin, instead of Jinja
- basic sanity test when running plugin standalone
- Improve testing by creating/binding to a bridge0 interface in
molecule docker container, then verifying that it's running
- Add ntp option
- multiplatform support
Change-Id: I7c2c3081e8919174dd29b3ab2fdd27b4f6eb843a
diff --git a/Makefile b/Makefile
index b16ce20..9ce7e2e 100644
--- a/Makefile
+++ b/Makefile
@@ -8,10 +8,13 @@
.DEFAULT_GOAL := help
.PHONY: test lint yamllint ansiblelint license help
-test: ## run tests on the playbook with molecule
+test: filter_test ## run tests on the playbook with molecule
molecule --version
molecule test
+filter_test: ## test filter plugins
+ python filter_plugins/rfc3442_words.py
+
lint: yamllint ansiblelint ## run all lint checks
# all YAML files
diff --git a/README.md b/README.md
index 48aa29a..bacd957 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,8 @@
- https://git.kernel.org/pub/scm/network/tftp/tftp-hpa.git
+Also supports OpenBSD dhcpd (fork of ISC) and tftpd (BSD).
+
## Reference docs
DHCP:
@@ -69,6 +71,10 @@
- dhcpd
```
+### Todo
+
+Add classless static route support for OpenBSD - see dhcp-options(5) on that
+system.
## License and Author
diff --git a/defaults/main.yml b/defaults/main.yml
index d0a5403..41d87ff 100644
--- a/defaults/main.yml
+++ b/defaults/main.yml
@@ -18,3 +18,7 @@
## tftpd config
# files to copy to the TFTP server
tftpd_files: []
+
+# Set this to listen on a specific non-default IP address. Only has effect on
+# OpenBSD TFTP server
+# tftpd_listen_ip: 10.0.0.1
diff --git a/filter_plugins/rfc3442_words.py b/filter_plugins/rfc3442_words.py
new file mode 100644
index 0000000..96756f5
--- /dev/null
+++ b/filter_plugins/rfc3442_words.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+
+# SPDX-FileCopyrightText: © 2021 Open Networking Foundation <support@opennetworking.org>
+# SPDX-License-Identifier: Apache-2.0
+
+# rfc3442_words.py
+# Input: a dict containing a router ip and multiple CIDR format routes
+# Output a list of octets to be appended to option 121 RFC3442 Classless routes option
+#
+# References:
+# https://tools.ietf.org/html/rfc3442
+# https://netaddr.readthedocs.io/en/latest/index.html
+
+from __future__ import absolute_import
+import netaddr
+
+
+class FilterModule(object):
+ def filters(self):
+ return {
+ "rfc3442_words": self.rfc3442_words,
+ }
+
+ def rfc3442_words(self, var):
+
+ words = []
+
+ router = var["ip"]
+ router_words = netaddr.IPNetwork(router).network.words
+
+ for r3442r in var["rfc3442routes"]:
+
+ # add prefix length
+ prefixlen = netaddr.IPNetwork(r3442r).prefixlen
+ words.append(prefixlen)
+
+ # add only the relevant portion of the address, depending ow words
+ (o1, o2, o3, o4) = netaddr.IPNetwork(r3442r).network.words
+ if prefixlen >= 25:
+ words += [o1, o2, o3, o4]
+ elif prefixlen >= 17:
+ words += [o1, o2, o3]
+ elif prefixlen >= 9:
+ words += [o1, o2]
+ elif prefixlen >= 1:
+ words += [o1]
+ # no additional words if prefixlen == 0
+
+ # add router address
+ words += list(router_words)
+
+ return words
+
+
+# test when running standalone outside of Ansible
+if __name__ == "__main__":
+
+ example = {
+ "ip": "192.168.1.10",
+ "rfc3442routes": [
+ "10.0.0.0/8",
+ "172.16.0.0/16",
+ "172.17.0.0/22",
+ "172.31.10.0/25",
+ ],
+ }
+
+ jfilter = FilterModule()
+
+ words = jfilter.rfc3442_words(example)
+
+ print(words)
+
+ # verify correct functionality
+ assert words == [
+ 8, 10, 192, 168, 1, 10,
+ 16, 172, 16, 192, 168, 1, 10,
+ 22, 172, 17, 0, 192, 168, 1, 10,
+ 25, 172, 31, 10, 0, 192, 168, 1, 10,
+ ]
diff --git a/handlers/main.yml b/handlers/main.yml
index 6fbce94..161f3e1 100644
--- a/handlers/main.yml
+++ b/handlers/main.yml
@@ -8,3 +8,8 @@
service:
name: "{{ dhcpd_service }}"
state: restarted
+
+- name: tftpd-restart
+ service:
+ name: "{{ tftpd_service }}"
+ state: restarted
diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml
index b95c131..13c2065 100644
--- a/molecule/default/converge.yml
+++ b/molecule/default/converge.yml
@@ -6,6 +6,33 @@
- name: Converge
hosts: all
+ vars:
+ dhcpd_interfaces:
+ - bridge0
+ dhcpd_subnets:
+ - subnet: "192.168.0.1/24"
+ range: "192.168.0.128/25"
+ dns_servers:
+ - "192.168.0.1"
+ - "192.168.0.2"
+ dns_search:
+ - "example.com"
+ tftpd_server: "192.168.0.1"
+ hosts:
+ - name: "dns"
+ ip_addr: "192.168.0.2"
+ mac_addr: "a1:b2:c3:d4:e5:f6"
+ - name: "extra_router"
+ ip_addr: "192.168.0.10"
+ mac_addr: "a6:b5:c4:d3:e2:f1"
+ routers:
+ - ip: "192.168.0.1"
+ - ip: "192.168.0.10"
+ rfc3442routes:
+ - 10.0.0.0/8
+ - 172.16.0.0/16
+ - 192.168.10.0/25
+
tasks:
- name: "Include dhcpd"
include_role:
diff --git a/molecule/default/prepare.yml b/molecule/default/prepare.yml
new file mode 100644
index 0000000..68ec77e
--- /dev/null
+++ b/molecule/default/prepare.yml
@@ -0,0 +1,22 @@
+---
+# dhcpd molecule/default/prepare.yml
+#
+# SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
+# SPDX-License-Identifier: Apache-2.0
+
+- name: Prepare
+ hosts: all
+
+ tasks:
+ - name: Update apt cache
+ apt:
+ update_cache: true
+
+ - name: Create a bridge to nowhere so dhcpd can start during testing
+ when: "'bridge0' not in ansible_interfaces"
+ command:
+ cmd: "{{ item }}"
+ with_items:
+ - "ip link add bridge0 type bridge"
+ - "ip addr add 192.168.0.5/24 dev bridge0"
+ - "ip link set bridge0 up"
diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml
index 1a4bfdc..aedd158 100644
--- a/molecule/default/verify.yml
+++ b/molecule/default/verify.yml
@@ -6,7 +6,12 @@
- name: Verify
hosts: all
+
tasks:
- - name: example assertion
+
+ - name: Populate service facts
+ service_facts:
+
+ - name: isc-dhcp-server is running
assert:
- that: true
+ that: ansible_facts.services["isc-dhcp-server.service"]["state"] == "running"
diff --git a/tasks/OpenBSD.yml b/tasks/OpenBSD.yml
new file mode 100644
index 0000000..7b33aa0
--- /dev/null
+++ b/tasks/OpenBSD.yml
@@ -0,0 +1,14 @@
+---
+# dhcpd tasks/OpenBSD.yml
+#
+# SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
+# SPDX-License-Identifier: Apache-2.0
+
+# installed in base, but need config
+
+- name: Set dhcpd and tftpd arguments for use with service module
+ set_fact:
+ dhcpd_arguments: "{{ dhcpd_interfaces | join(' ') }}"
+ tftpd_arguments: >
+ -v -l {{ tftpd_listen_ip | default(ansible_default_ipv4["address"]) }}
+ {{ tftpd_boot_dir }}
diff --git a/tasks/main.yml b/tasks/main.yml
index a030609..80927ec 100644
--- a/tasks/main.yml
+++ b/tasks/main.yml
@@ -17,7 +17,7 @@
backup: true
mode: "0644"
owner: root
- group: root
+ group: "{{ dhcpd_groupname }}"
# validate: 'dhcpd -t -cf %s' # Does not work...
notify:
- dhcpd-restart
@@ -36,9 +36,11 @@
name: "{{ dhcpd_service }}"
enabled: true
state: started
+ arguments: "{{ dhcpd_arguments | default(omit) }}"
- name: Enable and start tftpd
service:
name: "{{ tftpd_service }}"
enabled: true
state: started
+ arguments: "{{ tftpd_arguments | default(omit) }}"
diff --git a/templates/dhcpd.conf.j2 b/templates/dhcpd.conf.j2
index bda9bec..3951fe5 100644
--- a/templates/dhcpd.conf.j2
+++ b/templates/dhcpd.conf.j2
@@ -9,7 +9,9 @@
max-lease-time {{ subnet.max_lease_time | default("480") }};
# option definitions
+{% if ansible_system == "Linux" %}
option rfc3442-classless-static-routes code 121 = array of integer 8;
+{% endif %}
{% for subnet in dhcpd_subnets %}
subnet {{ subnet.subnet | ipaddr('network') }} netmask {{ subnet.subnet | ipaddr('netmask') }} {
@@ -18,13 +20,15 @@
{% if subnet.routers is defined %}
# custom router IP set
option routers {{ subnet.routers | map(attribute="ip") | join (",") }};
+{% set r3442ns = namespace(r3442list = []) %}
{% for rtr in subnet.routers %}
{% if "rfc3442routes" in rtr %}
-{% for r3442r in rtr.rfc3442routes %}
- option rfc3442-classless-static-routes {{ r3442r | ipaddr('prefix') }}, {{ r3442r | ipaddr('network') | regex_replace('\.', ', ')}}, {{ rtr.ip | ipaddr('network') | regex_replace('\.', ', ')}};
-{% endfor %}
+{% set r3442ns.r3442list = r3442ns.r3442list + (rtr | rfc3442_words() ) %}
{% endif %}
{% endfor %}
+{% if r3442ns.r3442list %}
+ option rfc3442-classless-static-routes {{ r3442ns.r3442list | join(', ') }};
+{% endif %}
{% else %}
# first IP address in range used as router
option routers {{ subnet.subnet | ipaddr('next_usable') }};
@@ -41,7 +45,13 @@
next-server {{ subnet.tftpd_server }};
{% endif %}
+{% if subnet.ntp_servers is defined %}
+ # ntp options
+ option ntp-servers {{ subnet.ntp_servers | join('", "') }};
+
+{% endif %}
{% if subnet.range is defined %}
+ # dynamically assignable range
range {{ subnet.range | ipaddr('next_usable') }} {{ subnet.range | ipaddr('last_usable') }};
{% endif %}
}
diff --git a/vars/Debian.yml b/vars/Debian.yml
index 5a1a5f8..01b6dc6 100644
--- a/vars/Debian.yml
+++ b/vars/Debian.yml
@@ -9,6 +9,7 @@
dhcpd_service: "isc-dhcp-server"
dhcpd_config_dir: "/etc/dhcp"
+dhcpd_groupname: "root"
tftpd_service: "tftpd-hpa"
tftpd_groupname: "tftp"
diff --git a/vars/OpenBSD.yml b/vars/OpenBSD.yml
new file mode 100644
index 0000000..d1ffefc
--- /dev/null
+++ b/vars/OpenBSD.yml
@@ -0,0 +1,16 @@
+---
+# dhcpd vars/OpenBSD.yml
+#
+# SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
+# SPDX-License-Identifier: Apache-2.0
+#
+# NOTE: Only put platform/OS-specific variables in this file.
+# Put all other variables in the 'defaults/main.yml' file.
+
+dhcpd_service: "dhcpd"
+dhcpd_config_dir: "/etc"
+dhcpd_groupname: "wheel"
+
+tftpd_service: "tftpd"
+tftpd_groupname: "_tftpd"
+tftpd_boot_dir: "/var/tftpboot"