Merge pull request #887 from kernelkit/dhcp-server

Add basic DHCP server support
This commit is contained in:
Mattias Walström
2025-01-31 15:11:07 +01:00
committed by GitHub
89 changed files with 3604 additions and 349 deletions
+9 -6
View File
@@ -3,16 +3,19 @@
# and similar events feed servers and configuration to dnsmasq.
domain-needed
# Only listen to loopback (local system)
interface=lo
bind-dynamic
#listen-address=127.0.0.1,::1
# Allow configuration and cache clear over D-Bus
enable-dbus
# Disable the following dnsmasq default DHCP options
#dhcp-option=option:netmask
#dhcp-option=28 # option:broadcast
#dhcp-option=option:domain-name
dhcp-option=option:router
dhcp-option=option:dns-server
dhcp-option=12 # option:hostname
# Generated by openresolv
resolv-file=/var/lib/misc/resolv.conf
# Include all files in a directory which end in .conf
conf-dir=/etc/dnsmasq.d/,*.conf
+39
View File
@@ -70,6 +70,7 @@ options:
-p Show plain output, no bells or whistles
commands:
dhcp Show DHCP server
port PORT Show port configuration and link information
ports Show ports available for bridging
vlans Show port groups in bridge
@@ -88,6 +89,41 @@ commands:
EOF
}
is_dhcp_running()
{
sysrepocfg -X -f json -m infix-dhcp-server | jq -r '
."infix-dhcp-server:dhcp-server".enabled as $global |
if ."infix-dhcp-server:dhcp-server".subnet? then
(."infix-dhcp-server:dhcp-server".subnet[] |
select(.enabled != false)) |
if $global != false and . then "true" else "false" end
else "false" end
' 2>/dev/null | grep -q true
}
dhcp()
{
if ! is_dhcp_running; then
echo "DHCP server not enabled."
exit 0
fi
case $1 in
detail)
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
jq -C .
;;
stat*)
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
/usr/libexec/statd/cli-pretty "show-dhcp-server" -s
;;
*)
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
/usr/libexec/statd/cli-pretty "show-dhcp-server"
;;
esac
}
# Usage 1: show port eth0
# Usage 2: show port
# Usage 3: show ports
@@ -295,6 +331,9 @@ case $cmd in
help)
usage
;;
dhcp | dhcp-server)
dhcp $*
;;
port*)
ports $*
;;
+52 -18
View File
@@ -3,35 +3,68 @@ Change Log
All notable changes to the project are documented in this file.
[v24.12.0][UNRELEASED]
[v25.01.0][] - 2025-01-31
-------------------------
> [!NOTE]
> This release contains breaking changes in the configuration file
> syntax for DHCP clients. Specifically DHCP options *with value*,
> i.e., the syntax for sending a hexadecimal value now require `hex`
> prefix before a string of colon-separated pairs of hex values.
### Changes
- NTP client status is now available in YANG
- Add support for more mDNS settings: allow/deny interfaces, acting
as "reflector" and filtering of reflected services. Issue #678
- Review of default `sysctl` settings, issue #829
- Upgrade Linux kernel to 6.12.11 (LTS)
- Upgrade Buildroot to 2024.02.10 (LTS)
- Add the possibility to change the boot order for the system with a
RPC and add boot order to operational datastore.
- SSH Server is now configurable, issue #441
SSH Server and NETCONF Server now uses the same SSH hostkey in factory-config
- Support for GRE/GRETAP tunnels
- Support for STP/RSTP on bridges
- Support for VXLAN tunnels
- Upgrade FRR from 9.1.2 to 9.1.3
- Add support for configuring SSH server, issue #441. As a result,
both SSH and NETCONF now use the same host key in `factory-config`
- Add operational support for reading DNS resolver info, issue #510
- Add operational support for NTP client, issue #510
- Add support for more mDNS settings: allow/deny interfaces, acting
as "reflector" and filtering of reflected services. Issue #678
- Add DHCPv4 server support, multiple subnets with static hosts and
DHCP options on global, subnet, or host level, issue #703.
Contributed by [MINEx Networks](https://minexn.com/)
- DHCP client options aligned with DHCP server, `startup-config`
files with old syntax are automatically migrated
- Breaking change in DHCP client options *with value*. Hexadecimal
values must now be formatted as `{ "hex": "c0:ff:ee" }` (JSON)
- Add documentation on management via SSH, Web (RESTCONF, Web
Console), and Console Port, issue #787
- Upgrade FRR from 9.1.2 to 9.1.3
- Add documentation of DNS client use and configuration, issue #798
- Add support for changing boot order for the system with an RPC,
including support for reading boot order from operational datastore
- Add support for GRE/GRETAP tunnels
- Add support for STP/RSTP on bridges
- Add support for VXLAN tunnels
### Fixes
- Fix #777: Authorized SSH key not applied to `startup-config`
- Fix #829: Avahi (mDNS responder) not starting properly on switches
with *many* ports (>10). This led to a review of `sysctl`:
- New for IPv4:
- Adjust IGMP max memberships: 20 -> 1000
- Use neighbor information on nexthop selection
- Use inbound interface address on ICMP errors
- Ignore routes with link down
- Disable `rp_filter`
- ARP settings have been changed to better fit routers, i.e.,
systems with multiple interfaces:
- Always use best local address when sending ARP
- Only reply to ARP if target IP is on the inbound interface
- Generate ARP requests when device is brought up or HW address changes
- New for IPv6:
- Keep static global addresses on link down
- Ignore routes with link down
- Fix #861: Fix error when running 251+ reconfigurations in test-mode
- Fix #777: Authorized SSH key not applied to startup config
- Minor cleanup of Networking Guide
- Fix memory leaks in confd
- Fix #869: Setup of bridges is now more robust
- Fix #899: DHCP client with client-id does not work
- Minor cleanup of Networking Guide
- Fix memory leaks in `confd`
[v24.11.1][] - 2024-11-29
-------------------------
@@ -81,6 +114,7 @@ All notable changes to the project are documented in this file.
forwarded, when the underlying ports are simultaneously attached to
a VLAN filtering bridge.
[v24.11.0][] - 2024-11-20
-------------------------
@@ -1437,8 +1471,8 @@ Supported YANG models in addition to those used by sysrepo and netopeer:
- N/A
[buildroot]: https://buildroot.org/
[UNRELEASED]: https://github.com/kernelkit/infix/compare/v24.11.0...HEAD
[v24.12.0]: https://github.com/kernelkit/infix/compare/v24.11.0...v24.12.0
[UNRELEASED]: https://github.com/kernelkit/infix/compare/v25.01.0...HEAD
[v25.01.0]: https://github.com/kernelkit/infix/compare/v24.11.0...v25.01.0
[v24.11.1]: https://github.com/kernelkit/infix/compare/v24.11.0...v24.11.1
[v24.11.0]: https://github.com/kernelkit/infix/compare/v24.10.0...v24.11.0
[v24.10.2]: https://github.com/kernelkit/infix/compare/v24.10.1...v24.10.2
+171
View File
@@ -0,0 +1,171 @@
DHCP Server
===========
The DHCPv4 server provides automatic IP address assignment and network
configuration for clients. It supports address pools, static host
assignments, and customizable DHCP options. It also serves as a DNS
proxy for local subnets and can even forward queries to upstream DNS
servers[^1].
> [!NOTE]
> When using the CLI, the system automatically enables essential options
> like DNS servers and default gateway based on the system's network
> configuration. These options can be disabled, changed or overridden,
> at any level: global, subnet, or per-host.
## Basic Configuration
The following example configures a DHCP server for subnet 192.168.2.0/24
with an address pool:
```
admin@example:/> configure
admin@example:/config/> edit dhcp-server
admin@example:/config/dhcp-server/> edit subnet 192.168.2.0/24
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> set pool start-address 192.168.2.100 end-address 192.168.2.200
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> leave
```
When setting up the server from the CLI, the system automatically adds a
few default DHCP options that will be sent to clients: both DNS server
and default gateway will use the system address on the matching
interface.
```
admin@example:/> show running-config
"infix-dhcp-server:dhcp-server": {
"subnet": [
{
"subnet": "192.168.2.0/24",
"option": [
{
"id": "dns-server",
"address": "auto"
},
{
"id": "router",
"address": "auto"
}
],
"pool": {
"start-address": "192.168.2.100",
"end-address": "192.168.2.200"
}
}
]
}
```
> [!IMPORTANT]
> Remember to set up an interface in this subnet, avoid using addresses
> in the DHCP pool, or reserved for static hosts. In Class C networks
> the router usually has address `.1`. Depending on the use-case, you
> may also want to set up routing.
## Static Host Assignment
To reserve specific IP addresses for clients based on their MAC address,
hostname, or client ID:
```
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> edit host 192.168.2.10
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/host/192.168.2.10/> set match mac-address 00:11:22:33:44:55
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/host/192.168.2.10/> set hostname printer
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/host/192.168.2.10/> leave
```
Match hosts using a client identifier instead of MAC address:
```
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/> edit host 192.168.1.50
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/host/192.168.1.50/> edit match
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/host/192.168.1.50/match/> set client-id hex c0:ff:ee
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/host/192.168.1.50/match/> leave
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/host/192.168.1.50/> set lease-time infinite
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/host/192.168.1.50/> leave
```
The `hex` prefix here ensures matching of client ID is done using the
hexadecimal octets `c0:ff:ee`, three bytes. Without the prefix the
ASCII string "c0:ff:ee", eight bytes, is used.
> [!NOTE]
> The DHCP server is fully RFC conformant, in the case of option 61 this
> means that using the `hex` prefix will require the client to set the
> `htype` field of the option to `00`. See RFC 2132 for details.
## Custom DHCP Options
Configure additional DHCP options globally, per subnet, or per host:
```
admin@example:/config/dhcp-server/> edit subnet 192.168.2.0/24
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> edit option dns-server
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/option/dns-server/> set address 8.8.8.8
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/option/dns-server/> leave
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> edit option ntp-server
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/option/ntp-server/> set address 192.168.2.1
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/option/ntp-server/> leave
```
When configuring, e.g., `dns-server`, or `router` options with the value
`auto`, the system uses the IP address from the interface matching the
subnet. For example:
```
admin@example:/> show interfaces brief
Interface Status Address
eth0 UP 192.168.1.1/24
eth1 UP 192.168.2.1/24
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/> edit option dns-server
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/option/dns-server/> set address auto
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/option/dns-server/> leave
```
In this case, clients in subnet 192.168.1.0/24 will receive 192.168.1.1
as their DNS server address.
## Multiple Subnets
Configure DHCP for multiple networks:
```
admin@example:/> configure
admin@example:/config/> edit dhcp-server
admin@example:/config/dhcp-server/> edit subnet 192.168.1.0/24
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/> set pool start-address 192.168.1.100 end-address 192.168.1.200
admin@example:/config/dhcp-server/subnet/192.168.1.0/24/> leave
admin@example:/config/dhcp-server/> edit subnet 192.168.2.0/24
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> set pool start-address 192.168.2.100 end-address 192.168.2.200
admin@example:/config/dhcp-server/subnet/192.168.2.0/24/> leave
```
## Monitoring
View active leases and server statistics:
```
admin@example:/> show dhcp-server
IP ADDRESS MAC HOSTNAME CLIENT ID EXPIRES
192.168.2.22 00:a0:85:00:02:05 00:c0:ff:ee 3591s
192.168.1.11 00:a0:85:00:04:06 foo 01:00:a0:85:00:04:06 3591s
admin@example:/> show dhcp-server statistics
DHCP offers sent : 6
DHCP ACK messages sent : 5
DHCP NAK messages sent : 0
DHCP decline messages received : 0
DHCP discover messages received : 6
DHCP request messages received : 5
DHCP release messages received : 6
DHCP inform messages received : 6
```
[^1]: This requires the system DNS resolver to be configured.
+21 -22
View File
@@ -664,31 +664,31 @@ are listed below. Configurable options can be disabled on a per client
interface basis, some options, like `clientid` and option 81, are
possible to set the value of as well.
| **Opt** | **Name** | **Cfg** | **Description** |
|---------|------------------|---------|-----------------------------------------------------|
| 1 | `subnet` | No | Request IP address and netmask |
| 3 | `router` | Yes | Default route(s), see also option 121 and 249 |
| 6 | `dns` | Yes | DNS server(s), static ones take precedence |
| 12 | `hostname` | Yes | DHCP cannot set hostname, only for informing server |
| 15 | `domain` | Yes | Default domain name, for name resolution |
| 28 | `broadcast` | Yes | Broadcast address, calculated if disabled |
| 42 | `ntpsrv` | Yes | NTP server(s), static ones take precedence |
| 50 | `address` | Yes | Request (previously cached) address |
| 61 | `clientid` | Yes | Default MAC address (and option 12) |
| 81 | `fqdn` | Yes | Similar to option 12, request FQDN update in DNS |
| 119 | `search` | Yes | Request domain search list |
| 121 | `staticroutes` | Yes | Classless static routes |
| 249 | `msstaticroutes` | Yes | Microsoft static route |
| | | | |
| **Opt** | **Name** | **Cfg** | **Description** |
|---------|-----------------------------|---------|-----------------------------------------------------|
| 1 | `netmask` | No | Request IP address and netmask |
| 3 | `router` | Yes | Default route(s), see also option 121 and 249 |
| 6 | `dns-server` | Yes | DNS server(s), static ones take precedence |
| 12 | `hostname` | Yes | DHCP cannot set hostname, only for informing server |
| 15 | `domain` | Yes | Default domain name, for name resolution |
| 28 | `broadcast` | Yes | Broadcast address, calculated if disabled |
| 42 | `ntp-server` | Yes | NTP server(s), static ones take precedence |
| 50 | `address` | Yes | Request (previously cached) address |
| 61 | `client-id` | Yes | Default MAC address (and option 12) |
| 81 | `fqdn` | Yes | Similar to option 12, request FQDN update in DNS |
| 119 | `search` | Yes | Request domain search list |
| 121 | `classless-static-route` | Yes | Classless static routes |
| 249 | `ms-classless-static-route` | Yes | Microsoft static route, same as option 121 |
| | | | |
**Default:** `router`, `dns`, `domain`, `broadcast`, `ntpsrv`, `search`,
`address`, `staticroutes`, `msstaticroutes`
**Default:** `router`, `dns-server`, `domain`, `broadcast`, `ntp-server`, `search`,
`address`, `classless-static-route`, `ms-classless-static-route`
When configuring a DHCP client, ensure that the NTP client is enabled
for the `ntpsrv` DHCP option to be processed correctly. If the NTP
for the `ntp-server` DHCP option to be processed correctly. If the NTP
client is not enabled, any NTP servers provided by the DHCP server will
be ignored. For details on how to enable the NTP client, see the
[NTP Client Configuration](system.md#ntp-client-configuration) section.
be ignored. For details on how to enable the NTP client, see the [NTP
Client Configuration](system.md#ntp-client-configuration) section.
> [!IMPORTANT]
> Per [RFC3442][4], if the DHCP server returns both a Classless Static
@@ -781,7 +781,6 @@ will be used, otherwise it falls back to the default algorithm.
admin@example:/> configure
admin@example:/config/> edit dhcp-client
admin@example:/config/dhcp-client/> set client-if eth0
admin@example:/config/dhcp-client/> set enabled true
admin@example:/config/dhcp-client/> leave
admin@example:/> show interfaces
INTERFACE PROTOCOL STATE DATA
+81 -28
View File
@@ -11,9 +11,10 @@ specific string followed by the last three octets of the system base MAC
address, e.g., `switch-12-34-56`. An example of how to change the
hostname is included below.
> **Note:** when issuing `leave` to activate your changes, remember to
> also save your settings, `copy running-config startup-config`. See
> the [CLI Introduction](cli/introduction.md) for a background.
> [!NOTE]
> When issuing `leave` to activate your changes, remember to also save
> your settings, `copy running-config startup-config`. See the [CLI
> Introduction](cli/introduction.md) for a background.
## Changing Password
@@ -39,7 +40,8 @@ the `do password encrypt` command. This launches the admin-exec command
to hash, and optionally salt, your password. This encrypted string can
then be used with `set password ...`.
> **Tip:** if you are having trouble thinking of a password, Infix has a
> [!TIP]
> If you are having trouble thinking of a password, there is a nifty
> `password generate` command in admin-exec context which generates
> random passwords using the UNIX command `pwgen`. Use the `do` prefix
> when inside any configuration context to access admin-exec commands.
@@ -65,9 +67,10 @@ key-data AAAAB3NzaC1yc2EAAAADAQABAAABgQC8iBL42yeMBioFay7lty1C4ZDTHcHyo739gc91rTT
admin@host:/config/system/authentication/user/admin/authorized-key/example@host/> leave
```
> **Note:** the `ssh-keygen` program already base64 encodes the public
> key data, so there is no need to use the `text-editor` command, `set`
> does the job.
> [!NOTE]
> The `ssh-keygen` program already base64 encodes the public key data,
> so there is no need to use the `text-editor` command, `set` does the
> job.
## Multiple Users
@@ -145,7 +148,7 @@ is committed by issuing the `leave` command.
admin@host:/config/> edit system
admin@host:/config/system/> set hostname example
admin@host:/config/system/> leave
admin@host:/>
admin@example:/>
```
The hostname is advertised over mDNS-SD in the `.local` domain. If
@@ -154,8 +157,24 @@ case, mDNS will advertise a "uniqified" variant, usually suffixing with
an index, e.g., `example-1.local`. Use an mDNS browser to scan for
available devices on your LAN.
> **Note:** critical services like syslog, mDNS, LLDP, and similar that
> advertise the hostname, are restarted when the hostname is changed.
In some cases you may want to set the device's *domain name* as well.
This is handled the same way:
```
admin@host:/config/> edit system
admin@host:/config/system/> set hostname foo.example.com
admin@host:/config/system/> leave
admin@foo:/>
```
Both host and domain name are stored in the system files `/etc/hosts`
and `/etc/hostname`. The latter is exclusively for the host name. The
domain *may* be used by the system DHCP server when handing out leases
to clients, it is up to the clients to request the domain name *option*.
> [!NOTE]
> Critical services like syslog, mDNS, LLDP, and similar that advertise
> the hostname, are restarted when the hostname is changed.
## Changing Login Banner
@@ -164,8 +183,9 @@ The `motd-banner` setting is an Infix augment and an example of a
`binary` type setting that can be changed interactively with the
built-in [`text-editor` command](cli/text-editor.md).
> **Tip:** see the next section for how to change the editor used
> to something you may be more familiar with.
> [!TIP]
> See the next section for how to change the editor used to something
> you may be more familiar with.
```
admin@host:/config/> edit system
@@ -196,24 +216,55 @@ admin@host:/config/system/> leave
admin@host:/>
```
> **Note:** as usual, configuration changes only take effect after
> issuing the `leave` command. I.e., you must change the editor first,
> and then re-enter configure context to use your editor of choice.
> [!IMPORTANT]
> Configuration changes only take effect after issuing the `leave`
> command. I.e., you must change the editor first, and then re-enter
> configure context to use your editor of choice.
## DNS Resolver Configuration
The system supports both static and dynamic (DHCP) DNS setup. The
locally configured (static) server is preferred over any acquired
from a DHCP client.
```
admin@host:/> configure
admin@host:/config/> edit system dns-resolver
admin@host:/config/system/dns-resolver/> set server google udp-and-tcp address 8.8.8.8
admin@host:/config/system/dns-resolver/> show
server google {
udp-and-tcp {
address 8.8.8.8;
}
}
admin@host:/config/system/dns-resolver/> leave
```
It is also possible to configure resolver options like timeout and
retry attempts. See the YANG model for details, or use the built-in
help system in the CLI.
> [!NOTE]
> When acting as a DHCP server and DNS proxy for other devices, any
> local DNS server configured here is automatically used as upstream DNS
> server.
## NTP Client Configuration
Below is an example configuration for enabling NTP
with a specific server and the `iburst` option for faster initial
synchronization.
Below is an example configuration for enabling NTP with a specific
server and the `iburst` option for faster initial synchronization.
```
admin@host:/> configure
admin@host:/config/> set system ntp enabled
admin@host:/config/> set system ntp server ntp-pool
admin@host:/config/> set system ntp server ntp-pool udp address pool.ntp.org
admin@host:/config/> set system ntp server ntp-pool iburst
admin@host:/config/> set system ntp server ntp-pool prefer
admin@host:/config/> edit system ntp
admin@host:/config/system/ntp/> set enabled
admin@host:/config/system/ntp/> set server ntp-pool
admin@host:/config/system/ntp/> set server ntp-pool udp address pool.ntp.org
admin@host:/config/system/ntp/> set server ntp-pool iburst
admin@host:/config/system/ntp/> set server ntp-pool prefer
admin@host:/config/system/ntp/> leave
```
This configuration enables the NTP client and sets the NTP server to
@@ -227,6 +278,7 @@ metrics (default config).
* `prefer true`: The NTP client will try to use the preferred server
as the primary source unless it becomes unreachable or unusable.
### Show NTP Sources
The status for NTP sources is availble in YANG and accessable with
@@ -242,8 +294,9 @@ ADDRESS MODE STATE STRATUM POLL-INTERVAL
```
### Show NTP Status
To check the status of NTP synchronization (only availble in CLI), use the following command:
To check the status of NTP synchronization (only availble in CLI), use
the following command:
```
admin@host:/> show ntp tracking
@@ -266,11 +319,11 @@ admin@host:/>
This output provides detailed information about the NTP status, including
reference ID, stratum, time offsets, frequency, and root delay.
> The system uses `chronyd` for Network Time Protocol (NTP)
> synchronization. The output shown here is best explained in the
> [Chrony documentation](https://chrony-project.org/doc/4.6.1/chronyc.html).
> [!TIP]
> The system uses `chronyd` Network Time Protocol (NTP) daemon. The
> output shown here is best explained in the [Chrony documentation][4].
[1]: https://www.rfc-editor.org/rfc/rfc7317
[2]: https://github.com/kernelkit/infix/blob/main/src/confd/yang/infix-system%402024-02-29.yang
[3]: https://www.rfc-editor.org/rfc/rfc8341
[4]: https://chrony-project.org/doc/4.6.1/chronyc.html
+1 -1
View File
@@ -4,7 +4,7 @@
#
################################################################################
CONFD_VERSION = 1.3
CONFD_VERSION = 1.4
CONFD_SITE_METHOD = local
CONFD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/confd
CONFD_LICENSE = BSD-3-Clause
+1
View File
@@ -1,5 +1,6 @@
config BR2_PACKAGE_PYTHON_STATD
bool "python-statd"
select BR2_PACKAGE_HOST_PYTHON3
select BR2_PACKAGE_DBUS_PYTHON
help
Python helpers for statd.
+1 -1
View File
@@ -3,7 +3,7 @@ PYTHON_STATD_SITE_METHOD = local
PYTHON_STATD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/statd/python
PYTHON_STATD_LICENSE = BSD-3-Clause
PYTHON_STATD_LICENSE_FILES = LICENSE
PYTHON_STATD_DEPENDENCIES = host-python3 python3 host-python-poetry-core
PYTHON_STATD_DEPENDENCIES = host-python3 python3 host-python-poetry-core dbus-python
PYTHON_STATD_SETUP_TYPE = pep517 # poetry
define PYTHON_STATD_MOVE_BINARIES
@@ -1 +1 @@
service [S12345] <pid/syslogd> dnsmasq -k -u root -- DHCP/DNS proxy
service [S12345] <!pid/syslogd> dnsmasq -k -u root -- DHCP/DNS proxy
@@ -0,0 +1,190 @@
commit 9ae2b2a466871d936ab79b15c95ba85cbc7d5cd6
Author: Stefan Schlosser <sgs@grmmbl.org>
Date: Fri Aug 23 10:52:17 2024 +0200
dnsmasq: add support for option 82
This patch adds support to insert Option 82 Relay Agent Information (see RFC3046)
into relayed DHCP requests.
When relay has been turned on with --dhcp-relay, the suboptions circuit-id and remote-id
can be configured using directives --dhcp-circuitid and --dhcp-remoteid with a maximum of
4 bytes denoted in hex format:
dhcp-circuitid=set:enterprise,00:11:22:33
dhcp-remoteid=set:enterprise,cc:dd:ee:ff
If circuit-id is not set, dnsmasq will use the interface index instead.
These options usually provide a way to map requests to --dhcp-host and --dhcp-range directives
but dhcp-proxy function uses them already for similar purposes.
The existing option space will be used (no re-negotiation). If it doesn't fit to a packet a warning
"Not enough space to add relay agent information"
will be dropped.
Signed-off-by: Stefan Schlosser <sgs@grmmbl.org>
diff --git dnsmasq-2.90/src/dhcp.c dnsmasq-2.90/src/dhcp.c
index b65facd..4c399eb 100644
--- dnsmasq-2.90/src/dhcp.c
+++ dnsmasq-2.90/src/dhcp.c
@@ -1117,7 +1117,10 @@ static int relay_upstream4(int iface_index, struct dhcp_packet *mess, size_t sz)
/* plug in our address */
mess->giaddr.s_addr = relay->local.addr4.s_addr;
}
-
+
+ /* add relay agent information */
+ add_relay_agent_info(relay, mess, sz);
+
to.sa.sa_family = AF_INET;
to.in.sin_addr = relay->server.addr4;
to.in.sin_port = htons(relay->port);
diff --git dnsmasq-2.90/src/dnsmasq.h dnsmasq-2.90/src/dnsmasq.h
index e455c3f..73cb94e 100644
--- dnsmasq-2.90/src/dnsmasq.h
+++ dnsmasq-2.90/src/dnsmasq.h
@@ -1902,3 +1902,6 @@ int add_update_server(int flags,
const char *interface,
const char *domain,
union all_addr *local_addr);
+
+
+int add_relay_agent_info(struct dhcp_relay *relay, struct dhcp_packet *mess, size_t sz);
diff --git dnsmasq-2.90/src/rfc2131.c dnsmasq-2.90/src/rfc2131.c
index 68834ea..05a4faa 100644
--- dnsmasq-2.90/src/rfc2131.c
+++ dnsmasq-2.90/src/rfc2131.c
@@ -2812,4 +2812,129 @@ static void apply_delay(u32 xid, time_t recvtime, struct dhcp_netid *netid)
}
}
+struct relay_info {
+ struct {
+ unsigned char *data;
+ int len;
+ } circuit;
+ struct {
+ unsigned char *data;
+ int len;
+ } remote;
+};
+
+static inline int subopt_add(unsigned char *ptr, size_t size,
+ int subopt, unsigned char *buf, int len)
+{
+ if ((len + 2) > size)
+ {
+ my_syslog(MS_DHCP | LOG_WARNING, "No space for relay sub option %d", subopt);
+ return 0;
+ }
+
+ *(ptr++) = subopt;
+ *(ptr++) = len;
+ memcpy(ptr, buf, len);
+
+ return (len + 2);
+}
+
+static int option_add(struct dhcp_packet *mess, size_t sz,
+ struct relay_info *info)
+{
+ unsigned char *start = &mess->options[0] + sizeof(u32);
+ unsigned char *end = (unsigned char *)mess + sz;
+ unsigned char *optend = NULL;
+ unsigned char opt[64], *sub, *p;
+ int size, len = 0;
+ int ret = -1;
+
+ sub = &opt[2];
+ size = sizeof(opt) - 2;
+
+ if (info->circuit.len > 0)
+ len += subopt_add(sub+len, size-len,
+ SUBOPT_CIRCUIT_ID,
+ info->circuit.data,
+ info->circuit.len);
+
+ if (info->remote.len > 0)
+ len += subopt_add(sub+len, size-len,
+ SUBOPT_REMOTE_ID,
+ info->remote.data,
+ info->remote.len);
+
+ if (len == 0)
+ return 0; /* Nothing to add */
+
+ opt[0] = OPTION_AGENT_ID;
+ opt[1] = len;
+ len += 2;
+
+ /* find option end */
+ optend = option_find1(start, end, OPTION_END, 1);
+ if (!optend)
+ return -1;
+
+ *optend = 0;
+
+ p = dhcp_skip_opts(start);
+ if ((p + len + 1) >= end)
+ {
+ my_syslog(MS_DHCP | LOG_WARNING, "Not enough space to add relay agent information");
+ goto out;
+ }
+
+ memcpy(p, opt, len);
+ optend = p + len;
+
+ ret = 0;
+out:
+ *optend = OPTION_END;
+ return ret;
+}
+
+int add_relay_agent_info(struct dhcp_relay *relay, struct dhcp_packet *mess, size_t sz)
+{
+ struct dhcp_vendor *vendor;
+ struct relay_info info;
+ int ifindex;
+
+ if (option_find(mess, sz, OPTION_AGENT_ID, 1))
+ return 0; /* don't alter any existing relay agent info */
+
+ /* lookup circuit-id and remote-id */
+ info.circuit.len = info.remote.len = 0;
+
+ for (vendor = daemon->dhcp_vendors; vendor; vendor = vendor->next)
+ {
+ if (vendor->match_type == MATCH_CIRCUIT)
+ {
+ if (info.circuit.len == 0)
+ {
+ info.circuit.data = vendor->data;
+ info.circuit.len = vendor->len;
+ }
+ }
+ else if (vendor->match_type == MATCH_REMOTE)
+ {
+ if (info.remote.len == 0)
+ {
+ info.remote.data = vendor->data;
+ info.remote.len = vendor->len;
+ }
+ }
+ }
+
+ /* use interface index as circuit-id if not specified */
+ if (info.circuit.len == 0)
+ {
+ ifindex = htonl(relay->iface_index);
+ info.circuit.data = &ifindex;
+ info.circuit.len = sizeof(ifindex);
+ }
+
+ return option_add(mess, sz, &info);
+}
+
#endif /* HAVE_DHCP */
+2 -1
View File
@@ -1,6 +1,6 @@
AC_PREREQ(2.61)
# confd version is same as system YANG model version, step on breaking changes
AC_INIT([confd], [1.3], [https://github.com/kernelkit/infix/issues])
AC_INIT([confd], [1.4], [https://github.com/kernelkit/infix/issues])
AM_INIT_AUTOMAKE(1.11 foreign subdir-objects)
AM_SILENT_RULES(yes)
@@ -17,6 +17,7 @@ AC_CONFIG_FILES([
share/migrate/1.1/Makefile
share/migrate/1.2/Makefile
share/migrate/1.3/Makefile
share/migrate/1.4/Makefile
src/Makefile
yang/Makefile
])
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# Rename DHCP client option attribute 'name' -> 'id'
# to match the DHCP server nomenclature.
file=$1
temp=${file}.tmp
jq '(
.["infix-dhcp-client:dhcp-client"]?."client-if"[]?.option[]? |=
if has("name") then
{ "id": .name } + . | del(.name)
else
.
end
)' "$file" > "$temp" &&
mv "$temp" "$file"
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
# Remap DHCP client options to match DHCP server nomenclature.
file=$1
temp=${file}.tmp
jq '(
.["infix-dhcp-client:dhcp-client"]?."client-if"[]?.option[]? |=
if has("id") then
.id = ({
"subnet" : "netmask",
"dns" : "dns-server",
"ntpsrv" : "ntp-server",
"clientid" : "client-id",
"staticroutes" : "classless-static-route",
"msstaticroutes" : "ms-classless-static-route"
}[.id] // .id)
else
.
end
)' "$file" > "$temp" &&
mv "$temp" "$file"
+3
View File
@@ -0,0 +1,3 @@
migratedir = $(pkgdatadir)/migrate/1.4
dist_migrate_DATA = 10-dhcp-client-option-name.sh \
20-dhcp-client-option-remap.sh
+1 -1
View File
@@ -1,2 +1,2 @@
SUBDIRS = 1.0 1.1 1.2 1.3
SUBDIRS = 1.0 1.1 1.2 1.3 1.4
migratedir = $(pkgdatadir)/migrate
+3 -1
View File
@@ -41,7 +41,9 @@ confd_plugin_la_SOURCES = \
ietf-syslog.c \
ietf-factory-default.c \
ietf-routing.c \
infix-dhcp.c \
infix-dhcp-common.c \
infix-dhcp-client.c \
infix-dhcp-server.c \
infix-factory.c \
infix-meta.c \
infix-services.c \
+4 -1
View File
@@ -146,7 +146,10 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
rc = infix_containers_init(&confd);
if (rc)
goto err;
rc = infix_dhcp_init(&confd);
rc = infix_dhcp_client_init(&confd);
if (rc)
goto err;
rc = infix_dhcp_server_init(&confd);
if (rc)
goto err;
rc = infix_factory_init(&confd);
+9 -3
View File
@@ -208,7 +208,7 @@ int ietf_syslog_init(struct confd *confd);
/* ietf-system.c */
int ietf_system_init (struct confd *confd);
int hostnamefmt (struct confd *confd, const char *fmt, char *buf, size_t len);
int hostnamefmt (struct confd *confd, const char *fmt, char *hostnm, size_t hostlen, char *domain, size_t domlen);
/* infix-containers.c */
#ifdef CONTAINERS
@@ -221,8 +221,14 @@ static inline void infix_containers_pre_hook(sr_session_ctx_t *session, struct c
static inline void infix_containers_post_hook(sr_session_ctx_t *session, struct confd *confd) {}
#endif
/* infix-dhcp.c */
int infix_dhcp_init(struct confd *confd);
/* infix-dhcp-common.c */
int dhcp_option_lookup(const struct lyd_node *id);
/* infix-dhcp-client.c */
int infix_dhcp_client_init(struct confd *confd);
/* infix-dhcp-server.c */
int infix_dhcp_server_init(struct confd *confd);
/* ietf-factory-default */
int ietf_factory_default_init(struct confd *confd);
+45 -23
View File
@@ -1399,23 +1399,23 @@ static int auth_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *modul
const char *path, const char *request_path, uint32_t request_id,
struct lyd_node **parent, void *priv)
{
const char *fmt = "/ietf-system:system/authentication/user[name='%s']/password";
struct spwd *spwd;
ERROR("%s() path %s reqeust_path %s", __func__, path, request_path);
setspent();
while ((spwd = getspent())) {
const char *fmt = "/ietf-system:system/authentication/user[name='%s']/password";
char xpath[256];
if (!spwd->sp_pwdp || spwd->sp_pwdp[0] == '*' || spwd->sp_pwdp[0] == '!')
/* Skip any records that do not pass YANG validation */
if (!spwd->sp_pwdp || spwd->sp_pwdp[0] == '0' ||
spwd->sp_pwdp[0] == '*' || spwd->sp_pwdp[0] == '!')
continue;
snprintf(xpath, sizeof(xpath), fmt, spwd->sp_namp);
lyd_new_path(*parent, NULL, xpath, spwd->sp_pwdp, 0, 0);
}
endspent();
return SR_ERR_OK;
}
@@ -1614,37 +1614,48 @@ static char *get_mac(struct confd *confd, char *mac, size_t len)
* NOTE: to be forward compatible, any unknown % combination will be silently consumed.
* E.g., "example-%z" will become "example-"
*/
int hostnamefmt(struct confd *confd, const char *fmt, char *buf, size_t len)
int hostnamefmt(struct confd *confd, const char *fmt, char *hostnm, size_t hostlen,
char *domain, size_t domlen)
{
char mac[10];
char *ptr;
size_t i;
if (!fmt || !*fmt)
return -1;
memset(buf, 0, len);
memset(hostnm, 0, hostlen);
if (domain)
memset(domain, 0, domlen);
ptr = strchr(fmt, '.');
if (ptr) {
*ptr++ = 0;
if (domain)
strlcpy(domain, ptr, domlen);
}
for (i = 0; i < strlen(fmt); i++) {
if (fmt[i] == '%') {
switch (fmt[++i]) {
case 'i':
strlcat(buf, id, len);
strlcat(hostnm, id, hostlen);
break;
case 'h':
strlcat(buf, nm, len);
strlcat(hostnm, nm, hostlen);
break;
case 'm':
strlcat(buf, get_mac(confd, mac, sizeof(mac)), len);
strlcat(hostnm, get_mac(confd, mac, sizeof(mac)), hostlen);
break;
case '%':
strlcat(buf, "%", len);
strlcat(hostnm, "%", hostlen);
break;
default:
break; /* Unknown, skip */
}
} else {
char ch[2] = { fmt[i], 0 };
strlcat(buf, ch, len);
strlcat(hostnm, ch, hostlen);
}
}
@@ -1656,8 +1667,8 @@ static int change_hostname(sr_session_ctx_t *session, uint32_t sub_id, const cha
{
struct confd *confd = (struct confd *)priv;
const char *hostip = "127.0.1.1";
char hostnm[65], buf[256];
char *fmt;
char hostnm[65], domain[65];
char buf[256], *fmt;
FILE *nfp, *fp;
int err, fd;
@@ -1665,11 +1676,13 @@ static int change_hostname(sr_session_ctx_t *session, uint32_t sub_id, const cha
return SR_ERR_OK;
fmt = srx_get_str(session, "%s", xpath);
if (!fmt) {
fallback:
strlcpy(hostnm, nm, sizeof(hostnm));
} else if (hostnamefmt(confd, fmt, hostnm, sizeof(hostnm)))
goto fallback;
if (!fmt)
fmt = strdup(nm);
if (hostnamefmt(confd, fmt, hostnm, sizeof(hostnm), domain, sizeof(domain))) {
err = SR_ERR_SYS;
goto err;
}
err = sethostname(hostnm, strlen(hostnm));
if (err) {
@@ -1678,6 +1691,11 @@ static int change_hostname(sr_session_ctx_t *session, uint32_t sub_id, const cha
goto err;
}
if (domain[0] && setdomainname(domain, strlen(domain))) {
ERROR("failed setting domain name");
/* Not cause for failing this function */
}
fp = fopen(_PATH_HOSTNAME, "w");
if (!fp) {
err = SR_ERR_INTERNAL;
@@ -1706,8 +1724,12 @@ static int change_hostname(sr_session_ctx_t *session, uint32_t sub_id, const cha
}
while (fgets(buf, sizeof(buf), fp)) {
if (!strncmp(buf, hostip, strlen(hostip)))
snprintf(buf, sizeof(buf), "%s\t%s\n", hostip, hostnm);
if (!strncmp(buf, hostip, strlen(hostip))) {
if (domain[0])
snprintf(buf, sizeof(buf), "%s\t%s.%s %s\n", hostip, hostnm, domain, hostnm);
else
snprintf(buf, sizeof(buf), "%s\t%s\n", hostip, hostnm);
}
fputs(buf, nfp);
}
@@ -1721,8 +1743,8 @@ static int change_hostname(sr_session_ctx_t *session, uint32_t sub_id, const cha
goto err;
/* Inform any running lldpd and avahi of the change ... */
systemf("lldpcli configure system hostname %s", hostnm);
systemf("avahi-set-host-name %s", hostnm);
systemf("initctl -bq status lldpd && lldpcli configure system hostname %s", hostnm);
systemf("initctl -bq status mdns && avahi-set-host-name %s", hostnm);
systemf("initctl -nbq touch netbrowse");
err:
if (fmt)
+1 -1
View File
@@ -67,7 +67,7 @@ static int add(const char *name, struct lyd_node *cif)
if ((string = lydx_get_cattr(cif, "hostname"))) {
char buf[65];
if (hostnamefmt(&confd, string, buf, sizeof(buf)))
if (hostnamefmt(&confd, string, buf, sizeof(buf), NULL, 0))
ERRNO("%s: failed setting custom hostname", name);
else
fprintf(fp, " --hostname %s", buf);
@@ -48,30 +48,32 @@ static char *ip_cache(const char *ifname, char *str, size_t len)
return str;
}
static char *hostname(const char *ifname, char *str, size_t len)
static char *hostname(struct lyd_node *cfg, char *str, size_t len)
{
FILE *fp;
int pos;
struct lyd_node *node;
const char *hostname;
char *ptr;
(void)ifname;
fp = fopen("/etc/hostname", "r");
if (!fp)
node = lydx_get_xpathf(cfg, "/ietf-system:system/hostname");
if (!node)
return NULL;
pos = snprintf(str, len, "-x hostname:");
if (!fgets(&str[pos], len - pos, fp))
str[0] = 0;
fclose(fp);
chomp(str);
strlcat(str, " ", len);
hostname = lyd_get_value(node);
if (!hostname || hostname[0] == 0)
return NULL;
ptr = strchr(hostname, '.');
if (ptr)
*ptr = 0;
snprintf(str, len, "-x hostname:%s ", hostname);
return str;
}
static char *fqdn(const char *value, char *str, size_t len)
static char *fqdn(const char *val, char *str, size_t len)
{
snprintf(str, len, "-F \"%s\" ", value);
snprintf(str, len, "-F \"%s\" ", val);
return str;
}
@@ -102,39 +104,46 @@ static char *os_name_version(char *str, size_t len)
return str;
}
static char *compose_option(const char *ifname, const char *name, const char *value,
char *option, size_t len)
static char *compose_option(struct lyd_node *cfg, const char *ifname, struct lyd_node *id,
const char *val, const char *hex, char *option, size_t len)
{
if (value) {
if (isdigit(name[0])) {
unsigned long opt = strtoul(name, NULL, 0);
const char *name = lyd_get_value(id);
int num = dhcp_option_lookup(id);
switch (opt) {
case 81:
return fqdn(value, option, len);
default:
break;
if (num == -1) {
ERROR("Failed looking up DHCP client %s option %s, skipping.", ifname, name);
return NULL;
}
if (val || hex) {
switch (num) {
case 81: /* fqdn */
if (!val)
return NULL;
return fqdn(val, option, len);
case 12: /* hostname */
if (val && !strcmp(val, "auto"))
return hostname(cfg, option, len);
/* fallthrough */
default:
if (hex) {
snprintf(option, len, "-x %d:", num);
strlcat(option, hex, len);
strlcat(option, " ", len);
} else {
/* string value */
snprintf(option, len, "-x %d:'\"%s\"' ", num, val);
}
snprintf(option, len, "-x %s:%s ", name, value);
} else {
if (!strcmp(name, "fqdn"))
fqdn(value, option, len);
else if (!strcmp(name, "hostname"))
snprintf(option, len, "-x %s:%s ", name, value);
else
snprintf(option, len, "-x %s:'\"%s\"' ", name, value);
break;
}
} else {
struct { char *name; char *(*cb)(const char *, char *, size_t); } opt[] = {
{ "hostname", hostname },
{ "address", ip_cache },
{ "fqdn", NULL },
{ NULL, NULL }
struct { int num; char *(*cb)(const char *, char *, size_t); } opt[] = {
{ 50, ip_cache }, /* address */
{ 81, NULL }, /* fqdn */
};
for (size_t i = 0; opt[i].name; i++) {
if (strcmp(name, opt[i].name))
for (size_t i = 0; i < NELEMS(opt); i++) {
if (num != opt[i].num)
continue;
if (!opt[i].cb || !opt[i].cb(ifname, option, len))
@@ -143,17 +152,20 @@ static char *compose_option(const char *ifname, const char *name, const char *va
return option;
}
snprintf(option, len, "-O %s ", name);
snprintf(option, len, "-O %d ", num);
}
return option;
}
static char *compose_options(const char *ifname, char **options, const char *name, const char *value)
static char *compose_options(struct lyd_node *cfg, const char *ifname, char **options,
struct lyd_node *id, const char *val, const char *hex)
{
char opt[300];
compose_option(ifname, name, value, opt, sizeof(opt));
if (!compose_option(cfg, ifname, id, val, hex, opt, sizeof(opt)))
return *options;
if (*options) {
char *opts;
@@ -171,29 +183,33 @@ static char *compose_options(const char *ifname, char **options, const char *nam
return *options;
}
static char *fallback_options(const char *ifname)
{
const char *defaults = "-O subnet -O broadcast -O router -O domain -O search "
"-O dns -O ntpsrv -O staticroutes -O msstaticroutes ";
char address[32] = { 0 };
char *options;
ip_cache(ifname, address, sizeof(address));
asprintf(&options, "%s %s", defaults, address);
return options;
}
static char *dhcp_options(const char *ifname, struct lyd_node *cfg)
{
struct lyd_node *option;
char *options = NULL;
LYX_LIST_FOR_EACH(lyd_child(cfg), option, "option") {
const char *value = lydx_get_cattr(option, "value");
const char *name = lydx_get_cattr(option, "name");
struct lyd_node *id = lydx_get_child(option, "id");
const char *val = lydx_get_cattr(option, "value");
const char *hex = lydx_get_cattr(option, "hex");
options = compose_options(ifname, &options, name, value);
options = compose_options(cfg, ifname, &options, id, val, hex);
}
if (!options) {
const char *defaults[] = {
"router", "dns", "domain", "broadcast", "ntpsrv", "search",
"address", "staticroutes", "msstaticroutes"
};
for (size_t i = 0; i < NELEMS(defaults); i++)
options = compose_options(ifname, &options, defaults[i], NULL);
}
return options ?: strdup("-O subnet -O router -O domain");
return options ?: fallback_options(ifname);
}
static void add(const char *ifname, struct lyd_node *cfg)
@@ -201,7 +217,7 @@ static void add(const char *ifname, struct lyd_node *cfg)
const char *metric = lydx_get_cattr(cfg, "route-preference");
const char *client_id = lydx_get_cattr(cfg, "client-id");
char vendor[128] = { 0 }, do_arp[20] = { 0 };
char *args = NULL, *options = NULL;
char *cid = NULL, *options = NULL;
const char *action = "disable";
bool arping;
FILE *fp;
@@ -211,55 +227,64 @@ static void add(const char *ifname, struct lyd_node *cfg)
snprintf(do_arp, sizeof(do_arp), "-a%d", ARPING_MSEC);
if (client_id && client_id[0]) {
args = alloca(strlen(client_id) + 12);
if (args)
sprintf(args, "-C -x 61:'\"%s\"'", client_id);
size_t len = 3 * strlen(client_id) + 16;
cid = malloc(len);
if (!cid)
goto generr;
strlcpy(cid, "-C -x 61:00", len);
for (size_t i = 0; client_id[i]; i++) {
char hex[5];
snprintf(hex, sizeof(hex), ":%02x", client_id[i]);
strlcat(cid, hex, len);
}
}
options = dhcp_options(ifname, cfg);
if (!options) {
ERROR("failed extracting DHCP options for client %s, aborting!", ifname);
goto err;
}
os_name_version(vendor, sizeof(vendor));
fp = fopenf("w", "/etc/finit.d/available/dhcp-%s.conf", ifname);
fp = fopenf("w", "/etc/finit.d/available/dhcp-client-%s.conf", ifname);
if (!fp) {
ERROR("failed creating DHCP client %s: %s", ifname, strerror(errno));
generr:
ERRNO("failed creating DHCP client %s: %s", ifname, strerror(errno));
goto err;
}
fprintf(fp, "# Generated by Infix confd\n");
fprintf(fp, "metric=%s\n", metric);
fprintf(fp, "service <!> name:dhcp :%s <net/%s/running> \\\n"
" [2345] udhcpc -f -p /run/dhcp-%s.pid -t 10 -T 3 -A 10 %s -S -R \\\n"
" -o %s \\\n"
fprintf(fp, "service <!> name:dhcp-client :%s <net/%s/running> \\\n"
" [2345] udhcpc -f -p /run/dhcp-client-%s.pid -t 10 -T 3 -A 10 %s -S -R \\\n"
" %s%s \\\n"
" -i %s %s %s \\\n"
" -- DHCP client @%s\n",
ifname, ifname, ifname, do_arp,
options,
ifname, args ?: "", vendor, ifname);
options ? "-o " : "", options,
ifname, cid ?: "", vendor, ifname);
fclose(fp);
action = "enable";
err:
systemf("initctl -bfqn %s dhcp-%s", action, ifname);
systemf("initctl -bfqn %s dhcp-client-%s", action, ifname);
if (options)
free(options);
if (cid)
free(cid);
}
static void del(const char *ifname)
{
systemf("initctl -bfq delete dhcp-%s", ifname);
systemf("initctl -bfq delete dhcp-client-%s", ifname);
}
static int change(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
const char *xpath, sr_event_t event, unsigned request_id, void *_confd)
{
struct lyd_node *global, *diff, *cifs, *difs, *cif, *dif;
sr_error_t err = 0;
sr_data_t *cfg;
int ena = 0;
sr_error_t err = 0;
sr_data_t *cfg;
int ena = 0;
switch (event) {
case SR_EV_DONE:
@@ -270,7 +295,7 @@ static int change(sr_session_ctx_t *session, uint32_t sub_id, const char *module
return SR_ERR_OK;
}
err = sr_get_data(session, XPATH "//.", 0, 0, 0, &cfg);
err = sr_get_data(session, "//.", 0, 0, 0, &cfg);
if (err || !cfg)
goto err_abandon;
@@ -328,18 +353,18 @@ err_abandon:
static void infer_options(sr_session_ctx_t *session, const char *xpath)
{
const char *opt[] = {
"subnet",
"router",
"dns",
"hostname", /* server may use this to register our current name */
"domain",
"netmask",
"broadcast",
"ntpsrv" /* will not be activated unless ietf-system also is */
"router",
"domain",
"hostname", /* server may use this to register our current name */
"dns-server",
"ntp-server" /* will not be activated unless ietf-system also is */
};
size_t i;
for (i = 0; i < NELEMS(opt); i++)
srx_set_item(session, NULL, 0, "%s/option[name='%s']", xpath, opt[i]);
srx_set_item(session, NULL, 0, "%s/option[id='%s']", xpath, opt[i]);
}
static int cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
@@ -396,7 +421,7 @@ static int cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
return SR_ERR_OK;
}
int infix_dhcp_init(struct confd *confd)
int infix_dhcp_client_init(struct confd *confd)
{
int rc;
+50
View File
@@ -0,0 +1,50 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#include <libyang/libyang.h>
int dhcp_option_lookup(const struct lyd_node *id)
{
const struct lysc_type_enum *enum_type;
const struct lysc_type_union *uni;
const struct lysc_node_leaf *leaf;
const struct lysc_node *schema;
const struct lysc_type *type;
LY_ARRAY_COUNT_TYPE u, e;
const char *name;
schema = id->schema;
if (!schema || schema->nodetype != LYS_LEAF)
return -1;
leaf = (const struct lysc_node_leaf *)schema;
type = leaf->type;
if (type->basetype != LY_TYPE_UNION)
return -1; /* We expect a union type */
uni = (const struct lysc_type_union *)type;
name = lyd_get_value(id);
/* Look through each type in the union */
for (u = 0; u < LY_ARRAY_COUNT(uni->types); u++) {
type = uni->types[u];
if (type->basetype == LY_TYPE_ENUM) {
enum_type = (const struct lysc_type_enum *)type;
for (e = 0; e < LY_ARRAY_COUNT(enum_type->enums); e++) {
if (!strcmp(enum_type->enums[e].name, name))
return enum_type->enums[e].value;
}
} else if (type->basetype == LY_TYPE_UINT8) {
char *endptr;
long val;
val = strtol(name, &endptr, 10);
if (*endptr == 0 && val > 0 && val < 255)
return (int)val;
}
}
return -1;
}
+445
View File
@@ -0,0 +1,445 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#include <assert.h>
#include <ctype.h>
#include <srx/common.h>
#include <srx/lyx.h>
#include <srx/srx_val.h>
#include "core.h"
#include <ifaddrs.h>
#define MODULE "infix-dhcp-server"
#define ROOT_XPATH "/infix-dhcp-server:"
#define CFG_XPATH ROOT_XPATH "dhcp-server"
#define DNSMASQ_GLOBAL_OPTS "/etc/dnsmasq.d/global-opts.conf"
#define DNSMASQ_SUBNET_FMT "/etc/dnsmasq.d/%s.conf"
#define DNSMASQ_LEASES "/var/lib/misc/dnsmasq.leases"
#define DBUS_NAME_DNSMASQ "uk.org.thekelleys.dnsmasq"
#define DEFAULT_LEASETIME 300 /* seconds */
#define MAX_LEASE_COUNT 1024 /* max. number of leases */
#define MAX_RELAY_SERVER 2 /* max. number of relay servers */
static const char *subnet_tag(const char *subnet)
{
unsigned int a, b, c, d, m;
static char tag[32];
sscanf(subnet, "%u.%u.%u.%u/%u", &a, &b, &c, &d, &m);
if (m == 8) /* Class A */
snprintf(tag, sizeof(tag), "net-%u", a);
else if (m == 16) /* Class B */
snprintf(tag, sizeof(tag), "net-%u-%u", a, b);
else if (m == 24) /* Class C */
snprintf(tag, sizeof(tag), "net-%u-%u-%u", a, b, c);
else {
/* Non-classful, use full format */
snprintf(tag, sizeof(tag), "net-%u-%u-%u-%u-%u",
a, b, c, d, m);
}
return tag;
}
static const char *host_tag(const char *subnet, const char *addr)
{
unsigned int a, b, c, d;
static char tag[128];
sscanf(addr, "%u.%u.%u.%u", &a, &b, &c, &d);
if (!subnet)
snprintf(tag, sizeof(tag), "host-%u-%u-%u-%u", a, b, c, d);
else
snprintf(tag, sizeof(tag), "%s-host-%u-%u-%u-%u", subnet, a, b, c, d);
return tag;
}
static int configure_options(FILE *fp, struct lyd_node *cfg, const char *tag)
{
struct lyd_node *option;
LYX_LIST_FOR_EACH(lyd_child(cfg), option, "option") {
struct lyd_node *id = lydx_get_child(option, "id");
struct lyd_node *suboption;
const char *val;
int num;
num = dhcp_option_lookup(id);
if (num == -1) {
ERROR("Unknown option %s", lyd_get_value(id));
return -1;
}
/*
* val is validated by the yang model for each of the various
* attributes 'address', 'name', and 'string'. in the future
* we may add 'value' as well, for hexadecimal data
*/
val = lydx_get_cattr(option, "name")
?: lydx_get_cattr(option, "string")
?: lydx_get_cattr(option, "hex")
?: NULL;
if (!val) {
val = lydx_get_cattr(option, "address");
if (val && !strcmp(val, "auto"))
val = "0.0.0.0";
}
if (val) {
fprintf(fp, "dhcp-option=%s%s%s%d,%s\n",
tag ? "tag:" : "", tag ?: "",
tag ? "," : "", num, val);
} else if ((suboption = lydx_get_descendant(option, "option", "static-route", NULL))) {
struct lyd_node *net;
LYX_LIST_FOR_EACH(suboption, net, "static-route") {
fprintf(fp, "dhcp-option=%s%s%s%d,%s,%s\n",
tag ? "tag:" : "",
tag ?: "", tag ? "," : "", num,
lydx_get_cattr(net, "destination"),
lydx_get_cattr(net, "next-hop"));
}
} else {
ERROR("Unknown value to option %s", lyd_get_value(id));
return -1;
}
}
return 0;
}
static const char *host_match(struct lyd_node *match, const char **id)
{
struct {
const char *key;
const char *prefix; /* dnsmasq prefix */
} choice[] = {
{ "mac-address", NULL },
{ "hostname", NULL },
{ "client-id", "id:" }
};
if (!match)
return NULL;
for (size_t i = 0; i < NELEMS(choice); i++) {
struct lyd_node *node, *sub;
const char *value;
node = lydx_get_child(match, choice[i].key);
if (!node)
continue;
*id = choice[i].prefix;
value = lyd_get_value(node);
if (value)
return value;
/* The client-id setting is has a qualifier */
sub = lydx_get_child(node, "str");
if (sub)
return lyd_get_value(sub);
sub = lydx_get_child(node, "hex");
if (sub)
return lyd_get_value(sub);
}
return NULL;
}
/* the address is a key in the host node, so is guaranteed to be set */
static int configure_host(FILE *fp, struct lyd_node *host, const char *subnet)
{
const char *ip = lydx_get_cattr(host, "address");
const char *tag = host_tag(NULL, ip);
const char *match, *id, *name;
match = host_match(lydx_get_child(host, "match"), &id);
if (!match)
return -1;
fprintf(fp, "\n# Host specific options\n");
if (configure_options(fp, host, tag))
return -1;
name = lydx_get_cattr(host, "hostname");
/*
* set host-specific tag, allow options from that tag,
* also allow options from subnet
*/
fprintf(fp, "\n# Host %s specific options\n", ip);
fprintf(fp, "dhcp-host=%s%s,set:%s,set:%s,%s,%s%s%s\n",
id ? id : "", match, tag, subnet, ip,
name ?: "", name ? "," : "",
lydx_get_cattr(host, "lease-time"));
return 0;
}
static void add(const char *subnet, struct lyd_node *cfg)
{
const char *tag = subnet_tag(subnet);
const char *val, *ifname;
struct lyd_node *node;
FILE *fp = NULL;
int rc = 0;
fp = fopenf("w", DNSMASQ_SUBNET_FMT, tag);
if (!fp) {
ERROR("Failed creating dnsmasq conf for %s: %s", subnet, strerror(errno));
return;
}
val = lydx_get_cattr(cfg, "description");
fprintf(fp, "# Subnet %s%s%s\n", subnet, val ? " - " : "", val ?: "");
if (debug)
fprintf(fp, "log-dhcp\n");
fprintf(fp, "\n# Common options for this subnet\n");
rc = configure_options(fp, cfg, tag);
if (rc)
goto err;
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "host") {
if ((rc = configure_host(fp, node, tag)))
goto err;
}
/* Optional, may be used to limit scope of subnet */
ifname = lydx_get_cattr(cfg, "if-name");
if ((node = lydx_get_child(cfg, "pool"))) {
const char *start, *end;
start = lydx_get_cattr(node, "start-address");
end = lydx_get_cattr(node, "end-address");
fprintf(fp, "\n# Subnet pool %s - %s\n", start, end);
fprintf(fp, "dhcp-range=%s%sset:%s,%s,%s,%s\n",
ifname ?: "", ifname ? "," : "", tag,
start, end, lydx_get_cattr(node, "lease-time"));
}
err:
fclose(fp);
}
static void del(const char *subnet, struct lyd_node *cfg)
{
struct in_addr subnet_addr;
FILE *fp, *next;
int prefix_len;
char line[512];
systemf("initctl -nbq stop dnsmasq");
fremove(DNSMASQ_SUBNET_FMT, subnet_tag(subnet));
/* Parse subnet/prefix */
if (sscanf(subnet, "%15[^/]/%d", line, &prefix_len) != 2)
goto parse_err;
if (inet_pton(AF_INET, line, &subnet_addr) != 1) {
parse_err:
ERRNO("Failed parsing DHCP server subnet %s for deletion", subnet);
return;
}
fp = fopen(DNSMASQ_LEASES, "r");
if (!fp)
return; /* Nothing to do here */
/* Create temp file for new leases */
next = fopen(DNSMASQ_LEASES"+", "w");
if (!next) {
ERRNO("Failed creating new leases file %s", DNSMASQ_LEASES"+");
fclose(fp);
return;
}
/* Copy non-matching leases */
while (fgets(line, sizeof(line), fp)) {
char mac[18], ip[16], name[64];
struct in_addr lease_addr;
unsigned int lease_time;
uint32_t subnet_mask;
if (sscanf(line, "%u %17s %15s %63s", &lease_time, mac, ip, name) < 3)
continue;
/* Check if IP is in subnet */
if (inet_pton(AF_INET, ip, &lease_addr) != 1)
continue;
subnet_mask = htonl(~((1UL << (32 - prefix_len)) - 1));
if ((lease_addr.s_addr & subnet_mask) == (subnet_addr.s_addr & subnet_mask))
continue; /* Skip matching lease */
fputs(line, next);
}
fclose(fp);
fclose(next);
/* Replace old leases file */
rename(DNSMASQ_LEASES"+", DNSMASQ_LEASES);
}
static int change(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
const char *xpath, sr_event_t event, unsigned request_id, void *_confd)
{
struct lyd_node *global, *diff, *cifs, *difs, *cif, *dif;
int enabled = 0, added = 0, deleted = 0;
sr_error_t err = 0;
sr_data_t *cfg;
switch (event) {
case SR_EV_DONE:
break;
case SR_EV_CHANGE:
case SR_EV_ABORT:
default:
return SR_ERR_OK;
}
err = sr_get_data(session, "//.", 0, 0, 0, &cfg);
if (err || !cfg)
goto err_abandon;
err = srx_get_diff(session, &diff);
if (err)
goto err_release_data;
global = lydx_get_descendant(cfg->tree, "dhcp-server", NULL);
enabled = lydx_is_enabled(global, "enabled");
cifs = lydx_get_descendant(cfg->tree, "dhcp-server", "subnet", NULL);
difs = lydx_get_descendant(diff, "dhcp-server", "subnet", NULL);
/* find the modified one, delete or recreate only that */
LYX_LIST_FOR_EACH(difs, dif, "subnet") {
const char *subnet = lydx_get_cattr(dif, "subnet");
if (lydx_get_op(dif) == LYDX_OP_DELETE) {
del(subnet, dif), deleted++;
continue;
}
LYX_LIST_FOR_EACH(cifs, cif, "subnet") {
const char *cnet = lydx_get_cattr(cif, "subnet");
if (strcmp(subnet, cnet))
continue;
if (!enabled || !lydx_is_enabled(cif, "enabled"))
del(subnet, cif), deleted++;
else
add(subnet, cif), added++;
break;
}
}
if (enabled) {
struct lyd_node *node;
FILE *fp;
fp = fopen(DNSMASQ_GLOBAL_OPTS, "w");
if (!fp)
goto err_done;
node = lydx_get_xpathf(cfg->tree, "/ietf-system:system/hostname");
if (node) {
const char *hostname = lyd_get_value(node);
const char *ptr = hostname ? strchr(hostname, '.') : NULL;
if (ptr)
fprintf(fp, "domain = %s\n", ++ptr);
}
err = configure_options(fp, global, NULL);
fclose(fp);
if (err)
goto err_done;
} else {
system("initctl -nbq stop dnsmasq"), deleted++;
erase(DNSMASQ_LEASES);
erase(DNSMASQ_GLOBAL_OPTS);
LYX_LIST_FOR_EACH(cifs, cif, "subnet") {
const char *subnet = lydx_get_cattr(cif, "subnet");
INFO("DHCP server globally disabled, stopping server on %s", subnet);
del(subnet, cif);
}
}
err_done:
if (deleted)
system("initctl -nbq restart dnsmasq");
else
system("initctl -nbq touch dnsmasq");
lyd_free_tree(diff);
err_release_data:
sr_release_data(cfg);
err_abandon:
return err;
}
static int cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
const char *path, sr_event_t event, unsigned request_id, void *priv)
{
const char *fmt = "/infix-dhcp-server:dhcp-server/option[id='%s']/address";
sr_val_t inferred = { .type = SR_STRING_T };
const char *opt[] = {
"router",
"dns-server",
};
size_t i, cnt = 0;
if (event != SR_EV_UPDATE && event != SR_EV_CHANGE)
return 0;
if (srx_nitems(session, &cnt, "/infix-dhcp-server:dhcp-server/option") || cnt)
return 0;
for (i = 0; i < NELEMS(opt); i++) {
inferred.data.string_val = "auto";
srx_set_item(session, &inferred, 0, fmt, opt[i]);
}
return 0;
}
static int clear_stats(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath,
const sr_val_t *input, const size_t input_cnt, sr_event_t event,
unsigned request_id, sr_val_t **output, size_t *output_cnt, void *priv)
{
if (systemf("dbus-send --system --dest=uk.org.thekelleys.dnsmasq "
"/uk/org/thekelleys/dnsmasq uk.org.thekelleys.dnsmasq.ClearMetrics"))
return SR_ERR_SYS;
return SR_ERR_OK;
}
int infix_dhcp_server_init(struct confd *confd)
{
int rc;
REGISTER_CHANGE(confd->session, MODULE, CFG_XPATH, 0, change, confd, &confd->sub);
REGISTER_CHANGE(confd->cand, MODULE, CFG_XPATH"//.", SR_SUBSCR_UPDATE, cand, confd, &confd->sub);
REGISTER_RPC(confd->session, CFG_XPATH "/statistics/clear", clear_stats, NULL, &confd->sub);
return SR_ERR_OK;
fail:
ERROR("init failed: %s", sr_strerror(rc));
return rc;
}
+4 -2
View File
@@ -30,9 +30,11 @@ MODULES=(
"infix-routing@2024-11-27.yang"
"ieee802-dot1ab-lldp@2022-03-15.yang"
"infix-lldp@2025-01-08.yang"
"infix-dhcp-client@2024-09-20.yang"
"infix-dhcp-common@2025-01-29.yang"
"infix-dhcp-client@2025-01-29.yang"
"infix-dhcp-server@2025-01-29.yang"
"infix-meta@2024-10-18.yang"
"infix-system@2024-11-27.yang"
"infix-system@2025-01-25.yang"
"infix-services@2024-12-03.yang"
"ieee802-ethernet-interface@2019-06-21.yang"
"infix-ethernet-interface@2024-02-27.yang"
+98 -84
View File
@@ -1,15 +1,25 @@
module infix-dhcp-client {
yang-version 1.1;
namespace "urn:ietf:params:xml:ns:yang:infix-dhcp-client";
prefix dhc4-clnt;
namespace "urn:ietf:params:xml:ns:yang:dhcp-client";
prefix dhcp-client;
import ietf-interfaces {
prefix "if";
prefix if;
}
import infix-dhcp-common {
prefix dhcp;
}
contact "kernelkit@googlegroups.com";
description "This module implements an IPv4 DHCP client";
description "This module implements a DHCPv4 client";
revision 2025-01-29 {
description "Consolidate DHCP options between client and server models.
- Rename option attribute 'name' -> 'id'
- Rename internal option type
- Rename options, e.g. ntpsrv -> ntp-server";
reference "internal";
}
revision 2024-09-20 {
description "Routes are installed in Frr (staticd), clarify preference
vs metric and adjust default preference 100 -> 5.";
@@ -37,121 +47,125 @@ module infix-dhcp-client {
description "This type is used for selecting route preference (distance).";
}
typedef dhcp-options {
type union {
type string;
type enumeration {
enum subnet {
value 1;
description "Subnet (IP address and netmask)";
}
enum router {
value 3;
description "Default route(s)";
}
enum dns {
value 6;
description "DNS server";
}
enum hostname {
value 12;
description "Hostname";
}
enum domain {
value 15;
description "Domain name";
}
enum broadcast {
value 28;
description "Broadcast address";
}
enum ntpsrv {
value 42;
description "NTP server";
}
enum address {
value 50;
description "Requested (previously cached) address";
}
enum clientid {
value 61;
description "Client ID (default MAC, and option 12)";
}
enum fqdn {
value 81;
description "Request DNS update of client FQDN argument";
}
enum search {
value 119;
description "Domain search list";
}
enum staticroutes {
value 121;
description "Classless static routes";
}
enum msstaticroutes {
value 249;
description "Microsoft classless static routes";
}
}
}
description "Supported DHCP client request options";
}
/*
* Data Nodes
*/
container dhcp-client {
description
"DHCPv4 client configuration";
description "DHCPv4 client configuration";
leaf enabled {
type boolean;
default "true";
description "Globally enables the DHCP client function.";
}
list client-if {
key "if-name";
description "List of interfaces requesting DHCPv4 configuration.";
key "if-name";
leaf if-name {
type if:interface-ref;
mandatory true;
description "Name of the interface.";
}
leaf enabled {
type boolean;
default "true";
description "Enable DHCP client for this interface.";
}
leaf client-id {
type string;
description "Optional Client ID, option 61, default: MAC address.";
description "Optional Client ID, option 61, RFC 2132.
When omitted the client sends its MAC address as client-id.
RFC 2132 defines client-id as [ OPT | LEN | TYPE | DATA ],
where TYPE is 0x01 for MAC address and 0x00 for anything
else. Servers usually just do a binary match of the data
using the length field. This particular setting encodes
the value as an ASCII string without trailing zero.
For full control of both type and data fields, use generic
option list instead, which supports entering raw HEX data.";
}
leaf arping {
type boolean;
default "true";
description "ARP for lease to check for IP address collisions (slow).";
}
list option {
key "name";
description
"List of DHCP options to request (and accept). The default is an
empty list, meaning all supported options. To restrict the
client to only get IP address and default route, set this to:
'subnet router'";
leaf name {
type dhcp-options;
key "id";
description "List of DHCP options to request (and accept).
The default is an empty list, meaning all supported options. To
restrict the client to only get IP address and default route, set
this to: 'subnet router'";
must "not(id = 'fqdn' and 'value/hex')" {
error-message "FQDN option must use string format";
error-app-tag "invalid-fqdn-format";
}
leaf id {
type dhcp:options;
description "DHCP option to request from, or inform server of.";
}
leaf value {
type string;
description "Optional value, only used for non-flag request options.
Example: option:hostname, value:xyzzy
option:clientid, value:01:02:03:04:05:06:07:08:09:0a
option:0x51, value:xyzzy.example.com";
must "../name != 'hostname' or re-match(., '[a-zA-Z0-9\\-_]{1,64}')";
choice value {
case value {
leaf value {
description "Optional value, to inform server, e.g., hostname.
Example 1)
option:12, value:xyzzy
Example 2)
option:hostname, value:xyzzy
Will be sent as [12 | 05 | 78 | 79 | 80 | 80 | 79],
which the server can use for static host matching.
For the 'hostname' option the 'auto' keyword can be
used to send the hostname part from IETF system.";
type string;
must "../id != 'hostname' or re-match(., '[a-zA-Z0-9\\-_]{1,64}')";
}
}
case hex {
leaf hex {
description "Optional binary value, to inform server, e.g., client-id.
This is a raw payload option allowing full control of
what is sent in an option. Often used with options 61
and option 43 (vendor-specific).
Example 1)
option:client-id, hex:00:c0:ff:ee
Will be sent as : [61 | 03 | 00 | c0 | ff | ee], which
is the RFC conformant formatting of option 61.
Example 2)
option:client-id, hex:c0:ff:ee
Will be sent as : [61 | 03 | c0 | ff | ee] meaning the
'htype' will be set to 'c0', while the RFC only defines
'00' (string) and and '01' (mac) for this field, some
users may want full control over this option.";
type dhcp:octet-string;
}
}
}
}
leaf route-preference {
type route-preference;
default 5;
+116
View File
@@ -0,0 +1,116 @@
module infix-dhcp-common {
yang-version 1.1;
namespace "urn:infix:params:xml:ns:yang:dhcp-common";
prefix dhcp;
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Shared types between DHCP server and client.";
revision 2025-01-29 {
description "Initial revision.";
reference "internal";
}
/*
* Typedefs
*/
typedef options {
description "DHCP options for client and server.";
type union {
type uint8 {
range "1..254";
}
type enumeration {
enum netmask {
value 1;
description "IP subnet mask as per RFC 950
Note: this option is always sent using a netmask derived from an
interface matching the given subnet, even if no configuration
exists in global/subnet/host scope. Except in the case when the
server responds to a relayed request, in which case the subnet
will not match any local interface.";
}
enum router {
value 3;
description "Default route, or gateway (single, see also option 121)";
}
enum dns-server {
value 6;
description "DNS server";
}
enum log-server {
value 7;
description "Name or IP address of remote syslog server";
}
enum hostname {
value 12;
description "Hostname";
}
enum domain {
value 15;
description "Domain name";
}
enum broadcast {
value 28;
description "IP broadcast address as per RFC 1122
Note: this option is always sent using a broadcast address from
an interface matching the given subnet, even if no configuration
exists in global/subnet/host scope. Except in the case when the
server responds to a relayed request, in which case the subnet will
not match any local interface.";
}
enum static-route {
value 33;
description "Classful (A/B/C network) route(s), use option 121 instead";
}
enum ntp-server {
value 42;
description "NTP server";
}
enum address {
value 50;
description "Request (previously cached) address";
}
enum client-id {
value 61;
description "Client ID (default MAC, and option 12)";
}
enum tftp-server {
value 66;
description "TFTP server";
}
enum bootfile {
value 67;
description "Boot filename from TFTP server (option 66)";
}
enum fqdn {
value 81;
description "Client request DNS update of FQDN argument, see RFC 4702";
}
enum search {
value 119;
description "Domain search list";
}
enum classless-static-route {
value 121;
description "Classless static route(s), tuple: subnet/len, next-hop";
}
enum ms-classless-static-route {
value 249;
description "Microsoft classless static route(s), same as option 121";
}
}
}
}
typedef octet-string {
description "A generic string or a hex values.";
type string {
pattern '([0-9a-fA-F]{2}:)*[0-9a-fA-F]{2}';
}
}
}
+1
View File
@@ -0,0 +1 @@
infix-dhcp-common.yang
+351
View File
@@ -0,0 +1,351 @@
module infix-dhcp-server {
yang-version 1.1;
namespace "urn:infix:params:xml:ns:yang:dhcp-server";
prefix dhcp-server;
import ietf-inet-types {
prefix inet;
}
import ietf-interfaces {
prefix if;
}
import ietf-yang-types {
prefix yang;
}
import infix-dhcp-common {
prefix dhcp;
}
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "This module implements a DHCPv4 server";
revision 2025-01-29 {
description "Initial revision adapted for Infix from DHCPv6 model.";
reference "internal";
}
typedef dhcp-lease-time {
description "The lease time in seconds, with a minimum of 2 minutes (120 seconds).
For static host entries, the value 'infinite' may be used.";
type union {
type uint32 {
range "120..max";
}
type enumeration {
enum infinite {
description "Infinite lease time for static host entries.";
}
}
}
units "seconds";
default "3600";
}
grouping dhcp-option-group {
description "Generic list structure for DHCP options.";
list option {
description "List of DHCP options.";
key "id";
leaf id {
description "DHCP option ID or number.";
type dhcp:options;
}
choice value {
case address-opt {
when "id = 'router' or id = 'dns-server' or id = 'log-server' or id = 'ntp-server' or id = 'netmask' or id = 'broadcast'";
leaf address {
description "IP address, or 'auto'.";
type union {
type inet:ipv4-address;
type enumeration {
enum auto {
description "Use IP address of the DHCP server.";
}
}
}
}
}
case name-opt {
when "id = 'hostname' or id = 'domain' or id = 'search'";
leaf name {
description "Internet name or FQDN.";
type inet:domain-name;
}
}
case classless-route-opt {
/* Support Microsoft's Classless Static Route option as well */
when "id = 'classless-static-route' or id = 121 or id = 249";
list static-route {
key "destination";
leaf destination {
description "Destination network prefix.";
type inet:ipv4-prefix;
}
leaf next-hop {
description "Next-hop IP address.";
type inet:ipv4-address;
}
}
}
case hex-opt {
leaf hex {
description "Binary data as colon-separated pairs of hex digits.
Example: c0:ff:ee";
type dhcp:octet-string;
}
}
case default-opt {
leaf string {
description "Generic string value.";
type string;
}
}
}
}
}
container dhcp-server {
description "DHCPv4 server configuration.";
leaf enabled {
description "Globally enable or disable the DHCP server.";
type boolean;
default true;
}
uses dhcp-option-group {
refine "option" {
description "Global DHCP options used as defaults for pools and static host leases.";
}
}
list subnet {
description "Subnet specific settings, including static host entries.";
key "subnet";
leaf subnet {
description "Subnet to serve DHCP leases from.";
type inet:ipv4-prefix;
}
leaf description {
description "Additional information about this subnet (e.g., purpose, location, or notes).";
type string;
}
leaf enabled {
description "Enable or disable DHCP server on this subnet.";
type boolean;
default true;
}
leaf if-name {
description "Optional interface to bind this subnet to.";
type if:interface-ref;
}
uses dhcp-option-group {
refine "option" {
description "List of DHCP options specific to this subnet.";
}
}
container pool {
description "IP address pool for this subnet.";
leaf start-address {
description "The start address of the DHCP address pool.";
type inet:ipv4-address;
}
leaf end-address {
description "The end address of the DHCP address pool.";
type inet:ipv4-address;
}
leaf lease-time {
description "Lease time for DHCP addresses.";
type dhcp-lease-time;
}
}
list host {
description "List of static host entries.";
key "address";
leaf address {
description "IPv4 address to assign to the client.";
type inet:ipv4-address;
mandatory true;
}
leaf description {
description "Additional information about this entry, e.g., owner's name, equipment details, or location.";
type string;
}
container match {
description "Match rule for this lease.";
choice match {
mandatory true;
case mac-address {
leaf mac-address {
description "Match on client MAC address";
type yang:mac-address;
}
}
case hostname {
leaf hostname {
description "Match on client hostname, DHCP option 12.";
type string;
}
}
case client-id {
container client-id {
description "Match on client-id, DHCP option 61.";
choice id {
description "Format of the client-id value";
default str;
case str {
leaf str {
description "String value for text-based client-id.
Example: xyzzy";
type string;
}
}
case hex {
leaf hex {
description "Binary data as colon-separated pairs of hex digits.
Example: c0:ff:ee";
type dhcp:octet-string;
}
}
}
}
}
/* case option82 here ... */
}
/* More advanced match rules, e.g., option82 + client-id are not supported. */
}
leaf hostname {
description "Optional hostname to assign with the lease.
Please note, Linux systems only allow 64 character hostnames.";
type inet:domain-name;
}
leaf lease-time {
description "Lease time, or 'infinite'.";
type dhcp-lease-time;
}
uses dhcp-option-group {
refine "option" {
description "List of DHCP options specific to this static host.
By default, global and subnet options are inherited.";
}
}
}
}
container leases {
description "Lease information.";
config false;
list lease {
description "List of active host leases.";
key address;
leaf address {
description "IP address of the host.";
type inet:ip-address;
}
leaf phys-address {
description "Physical (MAC) address of the host.";
type yang:mac-address;
}
leaf hostname {
description "Name of the host, if one was provided.";
type string;
}
leaf expires {
description "Time and date when the lease expires, or 'never'.
The value 'never' is used for infinite lease time, i.e., no expiration time.";
type union {
type yang:date-and-time;
type enumeration {
enum never;
}
}
}
leaf client-id {
description "Client identifier of the host, if one was provided.";
type string;
}
}
}
container statistics {
description "DHCP packet statistics.";
config false;
leaf out-offers {
type yang:counter32;
description "Total number of DHCPOFFER packets.";
}
leaf out-acks {
type yang:counter32;
description "Total number of DHCPACK packets.";
}
leaf out-naks {
type yang:counter32;
description "Total number of DHCPNAK packets.";
}
leaf in-declines {
type yang:counter32;
description "Total number of DHCPDECLINE packets.";
}
leaf in-discovers {
type yang:counter32;
description "Total number of DHCPDISCOVER packets.";
}
leaf in-requests {
type yang:counter32;
description "Total number of DHCPREQUEST packets.";
}
leaf in-releases {
type yang:counter32;
description "Total number of DHCPRELEASE packets.";
}
leaf in-informs {
type yang:counter32;
description "Total number of DHCPINFORM packets.";
}
action clear {
description "Clear packet statistics.";
}
}
}
}
+1
View File
@@ -0,0 +1 @@
infix-dhcp-server.yang
+83 -2
View File
@@ -6,6 +6,12 @@ module infix-system {
import ietf-system {
prefix sys;
}
import ietf-interfaces {
prefix if;
}
import ietf-yang-types {
prefix yang;
}
import iana-timezones {
prefix iana-tz;
}
@@ -22,8 +28,12 @@ module infix-system {
contact "kernelkit@googlegroups.com";
description "Infix augments and deviations to ietf-system.";
revision 2025-01-25 {
description "Add DNS resolver status.";
reference "internal";
}
revision 2024-11-27 {
description "Add NTP status";
description "Add NTP status.";
reference "internal";
}
revision 2024-09-13 {
@@ -237,7 +247,7 @@ module infix-system {
+ '|%[him]'
+ ')'
+ '([a-zA-Z0-9\-_\.]|%[him])*';
length "1..64";
length "1..128";
}
description "Linux have the same restrictions as IETF, only shorter.
Format specifiers are for, default hostname, ID, and the
@@ -305,6 +315,8 @@ module infix-system {
augment "/sys:system-state" {
container ntp {
description "NTP status";
config false;
container sources {
list source {
key address;
@@ -328,11 +340,80 @@ module infix-system {
type uint8;
description "Interval between transmitted NTP messages, expressed as a power of two in seconds";
}
}
}
}
container dns-resolver {
description "List of active DNS servers";
config false;
container options {
description "Resolver options.";
leaf timeout {
description "Number of seconds before resolves tries another server.";
type uint8;
units "seconds";
}
leaf attempts {
description "Number of times the resolver reties before giving up.";
type uint8;
}
}
leaf-list search {
type inet:domain-name;
description "Ordered list of domains to search when resolving a host name.";
}
list server {
key "address";
leaf address {
description "IP address of DNS server";
type inet:ip-address;
}
leaf origin {
description "How DNS server was acquired";
type enumeration {
enum static {
description "Statically configured";
}
enum dhcp {
description "Dynamically acquired via DHCP";
}
}
}
leaf interface {
type if:interface-ref;
description "Interface DNS server was learned from, if DHCP";
}
}
container statistics {
description "DNS resolver statistics";
leaf cache-size {
description "Current number of entries in DNS cache";
type yang:counter32;
}
leaf cache-hits {
description "Number of successful cache lookups";
type yang:counter64;
}
leaf cache-misses {
description "Number of failed cache lookups";
type yang:counter64;
}
}
}
}
deviation "/sys:system/sys:hostname" {
deviate replace {
type infix-sys:hostname;
+23 -3
View File
@@ -223,6 +223,12 @@
</COMMAND>
</COMMAND>
<COMMAND name="dhcp-server" help="DHCP server tools" mode="switch">
<COMMAND name="clear-statistics" help="Clear DHCP server statistics">
<ACTION sym="srp_rpc@sysrepo">/infix-dhcp-server:dhcp-server/statistics/clear</ACTION>
</COMMAND>
</COMMAND>
<COMMAND name="show" help="Show system status and configuration files" mode="switch">
<COMMAND name="bridge" help="Show bridge (ports/fdb/mdb/vlans)">
@@ -286,9 +292,8 @@
<COMMAND name="ntp" help="Show NTP (client) status">
<ACTION sym="script">
sysrepocfg -d operational -X -f json -x \
/ietf-system:system-state/infix-system:ntp | \
/usr/libexec/statd/cli-pretty "show-ntp"
sysrepocfg -d operational -X -f json -x /system-state/ntp | \
/usr/libexec/statd/cli-pretty "show-ntp"
</ACTION>
<SWITCH name="optional" min="0">
<COMMAND name="tracking" help="Show NTP tracking">
@@ -297,6 +302,21 @@
</SWITCH>
</COMMAND>
<COMMAND name="dhcp-server" help="Show DHCP server status">
<ACTION sym="script" in="tty" out="tty" interrupt="true">show dhcp</ACTION>
<SWITCH name="optional" min="0">
<COMMAND name="statistics" help="Show DHCP server statistics">
<ACTION sym="script" in="tty" out="tty" interrupt="true">show dhcp stat</ACTION>
</COMMAND>
<COMMAND name="detailed" help="Detailed (full operational) output">
<ACTION sym="script" in="tty" out="tty" interrupt="true">show dhcp detail</ACTION>
</COMMAND>
</SWITCH>
</COMMAND>
<COMMAND name="software" help="Show software info">
<SWITCH name="optional" min="0" max="1">
<COMMAND name="name" help="Show detailed info about a partition">
+6 -4
View File
@@ -119,8 +119,9 @@ int srx_free_changes(struct lyd_node *tree)
/* check if string at xpath (fmt) is set (non-zero length) */
bool srx_isset(sr_session_ctx_t *session, const char *fmt, ...)
{
sr_val_t *value = NULL;
bool isset = false;
sr_data_t *data;
const char *s;
char *xpath;
va_list ap;
size_t len;
@@ -137,12 +138,13 @@ bool srx_isset(sr_session_ctx_t *session, const char *fmt, ...)
vsnprintf(xpath, len, fmt, ap);
va_end(ap);
if (sr_get_item(session, xpath, 0, &value))
if (sr_get_node(session, xpath, 0, &data))
return false;
if (value->data.string_val[0] != 0)
s = lyd_get_value(data->tree);
if (s[0] != 0)
isset = true;
sr_free_val(value);
sr_release_data(data);
return isset;
}
+107 -2
View File
@@ -57,6 +57,12 @@ class PadSoftware:
state = 10
version = 23
class PadDhcpServer:
ip = 17
mac = 19
host = 21
cid = 19
exp = 10
class PadUsbPort:
title = 30
@@ -350,6 +356,81 @@ class STPPortID:
)
return f"{prio:1x}.{pid:03x}"
class DhcpServer:
def __init__(self, data):
self.data = data
self.leases = []
now = datetime.now(timezone.utc)
for lease in get_json_data([], self.data, 'leases', 'lease'):
if lease["expires"] == "never":
exp = " never"
else:
dt = datetime.strptime(lease['expires'], '%Y-%m-%dT%H:%M:%S%z')
seconds = int((dt - now).total_seconds())
exp = f" {self.format_duration(seconds)}"
self.leases.append({
"ip": lease["address"],
"mac": lease["phys-address"],
"cid": lease["client-id"],
"host": lease["hostname"],
"exp": exp
})
stats = get_json_data([], self.data, 'statistics')
self.out_offers = stats["out-offers"]
self.out_acks = stats["out-acks"]
self.out_naks = stats["out-naks"]
self.in_declines = stats["in-declines"]
self.in_discovers = stats["in-discovers"]
self.in_requests = stats["in-requests"]
self.in_releases = stats["in-releases"]
self.in_informs = stats["in-informs"]
def format_duration(self, seconds):
"""Convert seconds to DDdHHhMMmSSs format, omitting zero values"""
if seconds < 0:
return "expired"
days, remainder = divmod(seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
parts = []
if days:
parts.append(f"{days}d")
if hours:
parts.append(f"{hours}h")
if minutes:
parts.append(f"{minutes}m")
if seconds or not parts:
parts.append(f"{seconds}s")
return "".join(parts)
def print(self):
for lease in self.leases:
ip = lease["ip"]
mac = lease["mac"]
cid = lease["cid"]
exp = lease['exp']
host = lease["host"][:20]
row = f"{ip:<{PadDhcpServer.ip}}"
row += f"{mac:<{PadDhcpServer.mac}}"
row += f"{host:<{PadDhcpServer.host}}"
row += f"{cid:<{PadDhcpServer.cid}}"
row += f"{exp:>{PadDhcpServer.exp - 1}}"
print(row)
def print_stats(self):
print(f"{'DHCP offers sent':<{32}}: {self.out_offers}")
print(f"{'DHCP ACK messages sent':<{32}}: {self.out_acks}")
print(f"{'DHCP NAK messages sent':<{32}}: {self.out_naks}")
print(f"{'DHCP decline messages received':<{32}}: {self.in_declines}")
print(f"{'DHCP discover messages received':<{32}}: {self.in_discovers}")
print(f"{'DHCP request messages received':<{32}}: {self.in_requests}")
print(f"{'DHCP release messages received':<{32}}: {self.in_discovers}")
print(f"{'DHCP inform messages received':<{32}}: {self.in_discovers}")
class Iface:
def __init__(self, data):
@@ -1020,8 +1101,8 @@ def show_hardware(json):
def show_ntp(json):
if not json.get("ietf-system:system-state"):
print(f"Error, top level \"ietf-system:system-state\" missing")
sys.exit(1)
print("NTP client not enabled.")
return
hdr = (f"{'ADDRESS':<{PadNtpSource.address}}"
f"{'MODE':<{PadNtpSource.mode}}"
f"{'STATE':<{PadNtpSource.state}}"
@@ -1038,6 +1119,25 @@ def show_ntp(json):
row += f"{source['poll']:>{PadNtpSource.poll}}"
print(row)
def show_dhcp_server(json, stats):
data = json.get("infix-dhcp-server:dhcp-server")
if not data:
print("DHCP server not enabled.")
return
server = DhcpServer(data)
if stats:
server.print_stats()
else:
hdr = (f"{'IP ADDRESS':<{PadDhcpServer.ip}}"
f"{'MAC':<{PadDhcpServer.mac}}"
f"{'HOSTNAME':<{PadDhcpServer.host}}"
f"{'CLIENT ID':<{PadDhcpServer.cid}}"
f"{'EXPIRES':>{PadDhcpServer.exp}}")
print(Decore.invert(hdr))
server.print()
def main():
global UNIT_TEST
@@ -1074,6 +1174,9 @@ def main():
parser_show_boot_order = subparsers.add_parser('show-boot-order', help='Show NTP sources')
parser_dhcp_srv = subparsers.add_parser('show-dhcp-server', help='Show DHCP server')
parser_dhcp_srv.add_argument("-s", "--stats", action="store_true", help="Show server statistics")
args = parser.parse_args()
UNIT_TEST = args.test
@@ -1091,6 +1194,8 @@ def main():
show_hardware(json_data)
elif args.command == "show-ntp":
show_ntp(json_data)
elif args.command == "show-dhcp-server":
show_dhcp_server(json_data, args.stats)
else:
print(f"Error, unknown command '{args.command}'")
sys.exit(1)
@@ -0,0 +1,4 @@
from .dhcp_server_status import main
if __name__ == "__main__":
main()
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/python3
"""
This script is used to query a single dnsmasq daemon via dbus
and fills/clears the infix-dhcp-server packet statistics.
"""
import argparse
import json
import dbus
DNSMASQ_NAME = "uk.org.thekelleys.dnsmasq"
DNSMASQ_IFACE = "uk.org.thekelleys.dnsmasq"
DNSMASQ_OBJECT = "/uk/org/thekelleys/dnsmasq"
def main():
parser = argparse.ArgumentParser(prog='dhcp-server-status')
parser.add_argument("-c", "--clear", action="store_true",
help="Clear DHCP server metrics")
args = parser.parse_args()
try:
bus = dbus.SystemBus()
dnsmasq = dbus.Interface(bus.get_object(DNSMASQ_NAME, DNSMASQ_OBJECT),
DNSMASQ_IFACE)
if args.clear:
print("Clearing metrics for DHCP server")
dnsmasq.ClearMetrics()
else:
print(json.dumps({"metrics": dnsmasq.GetMetrics()}))
except dbus.DBusException as e:
print(f"Error: Unable to connect to dnsmasq via D-Bus: {e}")
if __name__ == "__main__":
main()
+5 -1
View File
@@ -70,14 +70,18 @@ def main():
elif args.model == 'infix-containers':
from . import infix_containers
yang_data = infix_containers.operational()
elif args.model == 'infix-dhcp-server':
from . import infix_dhcp_server
yang_data = infix_dhcp_server.operational()
elif args.model == 'ietf-system':
from . import ietf_system
yang_data = ietf_system.operational()
else:
common.LOG.warning(f"Unsupported model {args.model}", file=sys.stderr)
common.LOG.warning("Unsupported model %s", args.model)
sys.exit(1)
print(json.dumps(yang_data, indent=2))
if __name__ == "__main__":
main()
+26 -10
View File
@@ -59,6 +59,19 @@ class Host(abc.ABC):
"""
pass
def read_multiline(self, path, default=None):
"""Get lines of content from path
Returns a list of lines from the file, or default if not readable.
"""
try:
txt = self.read(path)
return txt.splitlines() if txt is not None else default
except:
if default is not None:
return default
raise
def read_json(self, path, default=None):
"""Get JSON object from path """
try:
@@ -92,7 +105,7 @@ class Localhost(Host):
def read(self, path):
try:
with open(path, 'r') as f:
with open(path, 'r', encoding='utf-8') as f:
data = f.read().strip()
return data
except FileNotFoundError:
@@ -116,7 +129,8 @@ class Remotehost(Localhost):
def now(self):
timestamp = self._run(["date", "-u", "+%s"], default=None, log=True)
if self.capdir:
with open(os.path.join(self.capdir, "timestamp"), "w") as f:
path = os.path.join(self.capdir, "timestamp")
with open(path, "w", encoding='utf-8') as f:
f.write(f"{timestamp}\n")
pass
@@ -127,7 +141,7 @@ class Remotehost(Localhost):
# arguments to a single string. Therefore, we must quoute
# arguments containing spaces so that commands like `vtysh -c
# "show ip route json"` work as expected.
cmd = " ".join([ arg if " " not in arg else f"\"{arg}\"" for arg in cmd ])
cmd = " ".join([arg if " " not in arg else f"\"{arg}\"" for arg in cmd])
return super().run(self.prefix + (cmd,), default, log)
def run(self, cmd, default=None, log=True):
@@ -136,12 +150,12 @@ class Remotehost(Localhost):
storedpath = os.path.join(self.capdir, "run", Replayhost.SlugOf(cmd))
if os.path.exists(storedpath):
with open(storedpath) as f:
with open(storedpath, 'r', encoding='utf-8') as f:
return f.read()
out = self._run(cmd, default, log)
with open(storedpath, "w") as f:
f.write(out)
with open(storedpath, "w", encoding='utf-8') as f:
f.write(out)
return out
@@ -150,8 +164,9 @@ class Remotehost(Localhost):
if self.capdir:
dirname = os.path.join(self.capdir, "rootfs", os.path.dirname(path[1:]))
filname = os.path.join(self.capdir, "rootfs", path[1:])
os.makedirs(dirname, exist_ok=True)
with open(os.path.join(self.capdir, "rootfs", path[1:]), "w") as f:
with open(filname, "w", encoding='utf-8') as f:
f.write(out)
return out
@@ -165,7 +180,8 @@ class Replayhost(Host):
self.replaydir = replaydir
def now(self):
with open(os.path.join(self.replaydir, "timestamp")) as f:
path = os.path.join(self.replaydir, "timestamp")
with open(path, 'r', encoding='utf-8') as f:
timestamp = f.read().strip()
return datetime.datetime.fromtimestamp(int(timestamp), datetime.timezone.utc)
@@ -173,7 +189,7 @@ class Replayhost(Host):
path = os.path.join(self.replaydir, "run", Replayhost.SlugOf(cmd))
try:
with open(path, 'r') as f:
with open(path, 'r', encoding='utf-8') as f:
return f.read()
except:
if default is not None:
@@ -186,7 +202,7 @@ class Replayhost(Host):
def read(self, path):
path = os.path.join(self.replaydir, "rootfs", path[1:])
try:
with open(path, 'r') as f:
with open(path, 'r', encoding='utf-8') as f:
return f.read()
except FileNotFoundError:
# This is considered OK
+62
View File
@@ -1,4 +1,5 @@
import subprocess
import ipaddress
from .common import insert
from .host import HOST
@@ -61,6 +62,65 @@ def add_ntp(out):
insert(out, "infix-system:ntp", "sources", "source", source)
def add_dns(out):
options = {}
servers = []
search = []
content = HOST.read_multiline("/etc/resolv.conf.head", [])
for line in content:
line = line.strip()
if line.startswith('nameserver'):
ip = line.split()[1]
try:
ipaddress.ip_address(ip)
servers.append({
"address": ip,
"origin": "static"
})
except ValueError:
continue
elif line.startswith('search'):
search.extend(line.split()[1:])
elif line.startswith('options'):
opts = line.split()[1:]
for opt in opts:
if opt.startswith('timeout:'):
options["timeout"] = int(opt.split(':')[1])
elif opt.startswith('attempts:'):
options["attempts"] = int(opt.split(':')[1])
output = HOST.run_multiline(['/sbin/resolvconf', '-l'], [])
for line in output:
line = line.strip()
if line.startswith('nameserver'):
parts = line.split('#', 1)
ip = parts[0].split()[1]
iface = None
if len(parts) > 1:
iface = parts[1].strip()
try:
ipaddress.ip_address(ip)
servers.append({
"address": ip,
"origin": "dhcp",
"interface": iface
})
except ValueError:
continue
elif line.startswith('search'):
search.extend(line.split()[1:])
insert(out, "infix-system:dns-resolver", "options", options)
insert(out, "infix-system:dns-resolver", "server", servers)
insert(out, "infix-system:dns-resolver", "search", search)
def add_software_slots(out, data):
slots = []
for slot in data["slots"]:
@@ -136,4 +196,6 @@ def operational():
add_software(out_state)
add_ntp(out_state)
add_dns(out_state)
return out
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""
Collect operational data for infix-dhcp-server.yang from dnsmasq
"""
from datetime import datetime, timezone
import json
import dbus
def leases(leases_file):
"""Populate DHCP leases table"""
table = []
try:
with open(leases_file, 'r', encoding='utf-8') as fd:
for line in fd:
tokens = line.strip().split(" ")
if len(tokens) != 5:
continue
# Handle infinite lease time as specified in RFC 2131
if tokens[0] == "0":
expires = "never"
else:
dt = datetime.fromtimestamp(int(tokens[0]),
tz=timezone.utc)
expires = dt.isoformat() + "+00:00"
row = {
"expires": expires,
"address": tokens[2],
"phys-address": tokens[1],
}
if tokens[3] != '*':
row["hostname"] = tokens[3]
else:
row["hostname"] = ""
if tokens[4] != '*':
row["client-id"] = tokens[4]
else:
row["client-id"] = ""
table.append(row)
except (IOError, OSError, ValueError):
pass
return table
def statistics():
"""Fetch DHCP server metrics over D-Bus"""
try:
bus = dbus.SystemBus()
obj = bus.get_object("uk.org.thekelleys.dnsmasq",
"/uk/org/thekelleys/dnsmasq")
srv = dbus.Interface(obj, "uk.org.thekelleys.dnsmasq")
metrics = srv.GetMetrics()
except dbus.exceptions.DBusException:
metrics = {
"dhcp_offer": 0,
"dhcp_ack": 0,
"dhcp_nak": 0,
"dhcp_decline": 0,
"dhcp_discover": 0,
"dhcp_request": 0,
"dhcp_release": 0,
"dhcp_inform": 0
}
return {
"out-offers": metrics["dhcp_offer"],
"out-acks": metrics["dhcp_ack"],
"out-naks": metrics["dhcp_nak"],
"in-declines": metrics["dhcp_decline"],
"in-discovers": metrics["dhcp_discover"],
"in-requests": metrics["dhcp_request"],
"in-releases": metrics["dhcp_release"],
"in-informs": metrics["dhcp_inform"]
}
def operational(leases_file="/var/lib/misc/dnsmasq.leases"):
"""Return operational status for DHCP server"""
return {
"infix-dhcp-server:dhcp-server": {
"statistics": statistics(),
"leases": {
"lease": leases(leases_file)
}
}
}
if __name__ == "__main__":
print(json.dumps(operational(leases_file="mock.leases")))
+4 -1
View File
@@ -40,6 +40,7 @@
#define XPATH_SYSTEM_BASE "/ietf-system:system-state"
#define XPATH_ROUTING_OSPF XPATH_ROUTING_BASE "/ospf"
#define XPATH_CONTAIN_BASE "/infix-containers:containers"
#define XPATH_DHCP_SERVER_BASE "/infix-dhcp-server:dhcp-server"
TAILQ_HEAD(sub_head, sub);
@@ -347,8 +348,10 @@ static int subscribe_to_all(struct statd *statd)
if (subscribe(statd, "infix-containers", XPATH_CONTAIN_BASE, sr_generic_cb))
return SR_ERR_INTERNAL;
#endif
INFO("Successfully subscribed to all models");
if (subscribe(statd, "infix-dhcp-server", XPATH_DHCP_SERVER_BASE, sr_generic_cb))
return SR_ERR_INTERNAL;
INFO("Successfully subscribed to all models");
return SR_ERR_OK;
}
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Route preference: DHCP vs Static
Route preference: DHCP vs Static
This test configures a device with both a DHCP-acquired route on a
dedicated interface and a static route to the same destination on
@@ -70,13 +70,13 @@ def config_target1(target, data1, data2, link):
"if-name": data1,
"enabled": True,
"option": [
{"name": "broadcast"},
{"name": "dns"},
{"name": "domain"},
{"name": "hostname"},
{"name": "ntpsrv"},
{"name": "router"},
{"name": "subnet"}
{"id": "broadcast"},
{"id": "dns-server"},
{"id": "domain"},
{"id": "hostname"},
{"id": "ntp-server"},
{"id": "router"},
{"id": "netmask"}
],
"route-preference": 5
}
+9 -3
View File
@@ -3,8 +3,14 @@
<<<
include::dhcp_basic/Readme.adoc[]
include::client_basic/Readme.adoc[]
include::dhcp_router/Readme.adoc[]
include::client_default_gw/Readme.adoc[]
include::dhcp_routes/Readme.adoc[]
include::client_routes/Readme.adoc[]
include::server_basic/Readme.adoc[]
include::server_host/Readme.adoc[]
include::server_subnets/Readme.adoc[]
+1
View File
@@ -0,0 +1 @@
client_basic.adoc
@@ -0,0 +1,25 @@
=== DHCP Basic
==== Description
This is a very basic DHCP test that requests an IPv4 lease
from a DHCP server and checks that the lease is set on the
interface.
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_dhcp/client_basic/topology.svg[DHCP Basic topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::client_basic/topology.svg[DHCP Basic topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[DHCP Basic topology]
endif::testgroup[]
endif::topdoc[]
==== Test sequence
. Initialize
. Verify client lease for 10.0.0.42
<<<
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
"""
DHCP Basic
"""DHCP Basic
This is a very basic DHCP test that requests an IPv4 lease
from a DHCP server and checks that the lease is set on the
interface.
"""
import infamy, infamy.dhcp
@@ -13,6 +13,7 @@ from infamy.util import until
with infamy.Test() as test:
ADDRESS = '10.0.0.42'
with test.step("Initialize"):
env = infamy.Env()
client = env.attach("client", "mgmt")
@@ -31,7 +32,7 @@ with infamy.Test() as test:
}
client.put_config_dict("infix-dhcp-client", config)
with test.step("Verify client get DHCP lease for 10.0.0.42 on client:data"):
with test.step("Verify client lease for 10.0.0.42"):
until(lambda: iface.address_exist(client, port, ADDRESS))
test.succeed()

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
client_default_gw.adoc
@@ -0,0 +1,27 @@
=== DHCP router
==== Description
Verify that the DHCP client receives default gateway (DHCP option 3,
router) and that route exists in operational datastore. The DHCP
server runs at 192.168.0.1 but should hand out option 3 to clients
with address 192.168.0.254.
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_dhcp/client_default_gw/topology.svg[DHCP router topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::client_default_gw/topology.svg[DHCP router topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[DHCP router topology]
endif::testgroup[]
endif::topdoc[]
==== Test sequence
. Initialize
. Set up DHCP server as 192.168.0.1, option 3: 192.168.0.254
. Verify DHCP client has default route via 192.168.0.254
<<<
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
"""
DHCP router
"""DHCP router
Verify that the DHCP client receives default gateway (DHCP option 3, router)
and that route exists in operational datastore.
Verify that the DHCP client receives default gateway (DHCP option 3,
router) and that route exists in operational datastore. The DHCP
server runs at 192.168.0.1 but should hand out option 3 to clients
with address 192.168.0.254.
"""
import infamy, infamy.dhcp
@@ -11,14 +12,17 @@ import infamy.route as route
from infamy.util import until
with infamy.Test() as test:
ROUTER = '192.168.0.254'
ROUTER = '192.168.0.254'
with test.step("Initialize"):
env = infamy.Env()
client = env.attach("client", "mgmt")
_, host = env.ltop.xlate("host", "data")
with infamy.IsolatedMacVlan(host) as netns:
netns.addip("192.168.0.1")
with test.step("Set up DHCP server as 192.168.0.1, option 3: 192.168.0.254"):
netns.addip("192.168.0.1")
with infamy.dhcp.Server(netns, router=ROUTER):
_, port = env.ltop.xlate("client", "data")
config = {
@@ -26,14 +30,14 @@ with infamy.Test() as test:
"client-if": [{
"if-name": f"{port}",
"option": [
{ "name": "router" }
{"id": "router"}
]
}]
}
}
client.put_config_dict("infix-dhcp-client", config)
with test.step("Verify client to set up default route via 192.168.0.254"):
with test.step("Verify DHCP client has default route via 192.168.0.254"):
until(lambda: route.ipv4_route_exist(client, "0.0.0.0/0", ROUTER))
test.succeed()

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

+1
View File
@@ -0,0 +1 @@
client_routes.adoc
@@ -0,0 +1,38 @@
=== DHCP option 121 vs option 3
==== Description
Verify that DHCP option 121 (classless static routes) is used over the
older option 3 (default gateway) in the DHCP client, when both are sent
by the server, see RFC3442. Use the routing RIB in the operational
datastore to verify.
Installing routes from a DHCP server should not affect already existing
(static) routes. To verify this, a static canary route (20.0.0.0/24 via
192.168.0.2) is installed before starting the DHCP client. As a twist,
this canary route has a next-hop (192.168.0.2) which is not reachable
until a DHCP lease has been acquired.
The DHCP server is set up to hand out both a default router (option 3)
via 192.168.0.1 and a default route (option 121) via 192.168.0.254.
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_dhcp/client_routes/topology.svg[DHCP option 121 vs option 3 topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::client_routes/topology.svg[DHCP option 121 vs option 3 topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[DHCP option 121 vs option 3 topology]
endif::testgroup[]
endif::topdoc[]
==== Test sequence
. Setting up canary route, 20.0.0.0/24 via 192.168.0.2
. Enabling DHCP client, allow option 3 and 121
. Verify client has route 10.0.0.0/24 via 192.168.0.254 (option 121)
. Verify client has default route via 192.168.0.254 (not use option 3)
. Verify client still has canary route to 20.0.0.0/24 via 192.168.0.2
<<<
@@ -64,8 +64,8 @@ with infamy.Test() as test:
"client-if": [{
"if-name": f"{port}",
"option": [
{"name": "router"},
{"name": "staticroutes"}
{"id": "router"},
{"id": "classless-static-route"}
]
}]
}
@@ -75,17 +75,17 @@ with infamy.Test() as test:
netns.addip("192.168.0.1")
print(f"Start DHCP server {ROUTER} with option 3 and 121")
with infamy.dhcp.Server(netns, prefix=PREFIX, router=ROUTER):
with test.step("Verify 'client' has a route 10.0.0.0/24 via 192.168.0.254 (option 121)"):
with test.step("Verify client has route 10.0.0.0/24 via 192.168.0.254 (option 121)"):
print("Verify client use classless routes, option 121")
until(lambda: route.ipv4_route_exist(client, PREFIX, ROUTER))
with test.step("Verify 'client' has default route via 192.168.0.254 (not use option 3)"):
with test.step("Verify client has default route via 192.168.0.254 (not use option 3)"):
print("Verify client did *not* use option 3")
if route.ipv4_route_exist(client, "0.0.0.0/0", ROUTER):
test.fail()
with test.step("Verify 'client' still has canary route to 20.0.0.0/24 via 192.168.0.2"):
with test.step("Verify client still has canary route to 20.0.0.0/24 via 192.168.0.2"):
until(lambda: route.ipv4_route_exist(client, CANARY,
CANHOP, pref=250))
CANHOP, pref=250))
test.succeed()

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -1 +0,0 @@
dhcp_basic.adoc
@@ -1 +0,0 @@
dhcp_router.adoc
@@ -1 +0,0 @@
dhcp_routes.adoc
+15 -6
View File
@@ -1,9 +1,18 @@
---
- name: dhcp_basic
case: dhcp_basic/test.py
- name: client_basic
case: client_basic/test.py
- name: dhcp_router
case: dhcp_router/test.py
- name: client_default_gw
case: client_default_gw/test.py
- name: dhcp_routes
case: dhcp_routes/test.py
- name: client_routes
case: client_routes/test.py
- name: server_basic
case: server_basic/test.py
- name: server_host
case: server_host/test.py
- name: server_subnets
case: server_subnets/test.py
+1
View File
@@ -0,0 +1 @@
server_basic.adoc
@@ -0,0 +1,26 @@
=== DHCP Server
==== Description
Verify basic DHCP server functionality.
==== Topology
ifdef::topdoc[]
image::../../test/case/infix_dhcp/dhcp_server/topology.svg[DHCP Server topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::dhcp_server/topology.svg[DHCP Server topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[DHCP Server topology]
endif::testgroup[]
endif::topdoc[]
==== Test sequence
. Set up topology and attach to client and server DUTs
. Configure DHCP client and server DUTs
. Verify DHCP client original hostname
. Verify DHCP client lease
. Verify DHCP client new hostname
<<<
@@ -0,0 +1,31 @@
=== DHCP Server Basic
==== Description
Verify basic DHCP server functionality. The server is set up to only
hand out leases from a pool of a single address. A single DHCP option,
hostname, is to be handed out with the lease to the client. Ensure no
other options are sent by checking, e.g., that the client has not set up
a default route to the server.
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_dhcp/server_basic/topology.svg[DHCP Server Basic topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::server_basic/topology.svg[DHCP Server Basic topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[DHCP Server Basic topology]
endif::testgroup[]
endif::topdoc[]
==== Test sequence
. Set up topology and attach to client and server DUTs
. Configure DHCP server and client
. Verify DHCP client's original hostname
. Verify DHCP client lease from server's pool
. Verify DHCP client's new hostname
. Verify DHCP client has no default route
<<<
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""DHCP Server Basic
Verify basic DHCP server functionality. The server is set up to only
hand out leases from a pool of a single address. A single DHCP option,
hostname, is to be handed out with the lease to the client. Ensure no
other options are sent by checking, e.g., that the client has not set up
a default route to the server.
"""
import infamy
import infamy.iface as iface
import infamy.route as route
from infamy.util import until
def verify_hostname(dut, hostname):
"""Verify dut hostname"""
oper = dut.get_data("/ietf-system:system")
curr = oper["system"]["hostname"]
# print(f"Hostname now: {curr}")
if curr == hostname:
return True
return False
with infamy.Test() as test:
ADDRESS = '192.168.2.100'
HOSTNM1 = 'foo'
HOSTNM2 = 'client'
with test.step("Set up topology and attach to client and server DUTs"):
env = infamy.Env()
client = env.attach("client", "mgmt")
server = env.attach("server", "mgmt")
with test.step("Configure DHCP server and client"):
server.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [{
"name": server["link"],
"ipv4": {
"address": [{
"ip": "192.168.2.1",
"prefix-length": 24
}]
}
}]
}
},
"infix-dhcp-server": {
"dhcp-server": {
"subnet": [{
"subnet": "192.168.2.0/24",
"option": [{
"id": "hostname",
"name": HOSTNM2
}],
"pool": {
"start-address": ADDRESS,
"end-address": ADDRESS,
}
}]
}
},
"ietf-system": {
"system": {"hostname": "server.example.com"}
}})
client.put_config_dicts({
"infix-dhcp-client": {
"dhcp-client": {
"client-if": [{
"if-name": client["link"],
"option": [
{"id": "hostname"},
{"id": "domain"}
]
}]
}
},
"ietf-system": {
"system": {"hostname": HOSTNM1}
}})
with test.step("Verify DHCP client's original hostname"):
until(lambda: verify_hostname(client, HOSTNM1))
with test.step("Verify DHCP client lease from server's pool"):
until(lambda: iface.address_exist(client, client["link"], ADDRESS))
with test.step("Verify DHCP client's new hostname"):
until(lambda: verify_hostname(client, HOSTNM2))
with test.step("Verify DHCP client has no default route"):
until(lambda: route.ipv4_route_exist(client, "0.0.0.0/0") is False)
test.succeed()
@@ -0,0 +1,33 @@
graph "2x2" {
layout="neato";
overlap="false";
esep="+60";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt1> mgmt1 | <mgmt2> mgmt2 }",
pos="0,12!",
requires="controller",
];
server [
label="{ <link> link | <mgmt> mgmt } | server",
pos="15,6!",
requires="infix",
];
client [
label="{ <mgmt> mgmt | <link> link} | client",
pos="15,18!",
requires="infix",
];
host:mgmt1 -- client:mgmt [requires=mgmt, color=lightgrey]
host:mgmt2 -- server:mgmt [requires=mgmt, color=lightgrey]
server:link -- client:link [color=black, fontcolor=black, taillabel="192.168.2.1/24"]
}
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 2x2 Pages: 1 -->
<svg width="382pt" height="265pt"
viewBox="0.00 0.00 382.03 264.62" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 260.62)">
<title>2x2</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-260.62 378.03,-260.62 378.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-105.31 0,-151.31 108,-151.31 108,-105.31 0,-105.31"/>
<text text-anchor="middle" x="25" y="-124.61" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-105.31 50,-151.31 "/>
<text text-anchor="middle" x="79" y="-136.11" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
<polyline fill="none" stroke="black" points="50,-128.31 108,-128.31 "/>
<text text-anchor="middle" x="79" y="-113.11" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
</g>
<!-- server -->
<g id="node2" class="node">
<title>server</title>
<polygon fill="none" stroke="black" points="258.03,-0.5 258.03,-46.5 374.03,-46.5 374.03,-0.5 258.03,-0.5"/>
<text text-anchor="middle" x="283.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="258.03,-23.5 308.03,-23.5 "/>
<text text-anchor="middle" x="283.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="308.03,-0.5 308.03,-46.5 "/>
<text text-anchor="middle" x="341.03" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">server</text>
</g>
<!-- host&#45;&#45;server -->
<g id="edge2" class="edge">
<title>host:mgmt2&#45;&#45;server:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-116.31C108,-116.31 258.03,-11.5 258.03,-11.5"/>
</g>
<!-- client -->
<g id="node3" class="node">
<title>client</title>
<polygon fill="none" stroke="black" points="258.03,-210.12 258.03,-256.12 374.03,-256.12 374.03,-210.12 258.03,-210.12"/>
<text text-anchor="middle" x="283.03" y="-240.92" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="258.03,-233.12 308.03,-233.12 "/>
<text text-anchor="middle" x="283.03" y="-217.92" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="308.03,-210.12 308.03,-256.12 "/>
<text text-anchor="middle" x="341.03" y="-229.42" font-family="DejaVu Sans Mono, Book" font-size="14.00">client</text>
</g>
<!-- host&#45;&#45;client -->
<g id="edge1" class="edge">
<title>host:mgmt1&#45;&#45;client:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-140.31C108,-140.31 258.03,-245.12 258.03,-245.12"/>
</g>
<!-- server&#45;&#45;client -->
<g id="edge3" class="edge">
<title>server:link&#45;&#45;client:link</title>
<path fill="none" stroke="black" stroke-width="2" d="M283.03,-46.5C283.03,-46.5 283.03,-210.12 283.03,-210.12"/>
<text text-anchor="middle" x="228.53" y="-50.3" font-family="DejaVu Serif, Book" font-size="14.00">192.168.2.1/24</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+1
View File
@@ -0,0 +1 @@
server_host.adoc
@@ -0,0 +1,31 @@
=== DHCP Server Static Host
==== Description
Verify DHCP server can hand out static host leases based on client-id,
both hexadecimal and a very long string, ensuring no pool address is
handed out instead.
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_dhcp/server_host/topology.svg[DHCP Server Static Host topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::server_host/topology.svg[DHCP Server Static Host topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[DHCP Server Static Host topology]
endif::testgroup[]
endif::topdoc[]
==== Test sequence
. Set up topology and attach to client and server DUTs
. Configure DHCP client and server DUTs
. Verify DHCP client1 static lease
. Verify client1 hostname has *not* changed
. Verify DHCP client1 has default route via option 121
. Verify DHCP client2 static lease
. Verify client2 hostname has changed
. Verify DHCP client2 has default gateway
<<<
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""DHCP Server Static Host
Verify DHCP server can hand out static host leases based on client-id,
both hexadecimal and a very long string, ensuring no pool address is
handed out instead.
"""
import infamy
import infamy.iface as iface
import infamy.route as route
from infamy.util import until
with infamy.Test() as test:
POOL1 = '192.168.1.100'
ADDRESS1 = '192.168.1.11'
GW1 = '192.168.1.2'
POOL2 = '192.168.2.100'
ADDRESS2 = '192.168.2.22'
GW2 = '192.168.2.1'
HOSTNM1 = 'foo'
HOSTCID1 = '00:c0:ff:ee' # Infix DHCP server is RFC compliant
HOSTNM11 = 'client1'
HOSTCID2 = 'xyzzydissiegillespiefoobarterrawinklesouponastick'
HOSTNM2 = 'bar'
HOSTNM22 = 'client2'
with test.step("Set up topology and attach to client and server DUTs"):
env = infamy.Env()
server = env.attach("server", "mgmt")
client1 = env.attach("client1", "mgmt")
client2 = env.attach("client2", "mgmt")
with test.step("Configure DHCP client and server DUTs"):
server.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [
{
"name": server["link1"],
"ipv4": {
"address": [{
"ip": "192.168.1.1",
"prefix-length": 24
}]
}
}, {
"name": server["link2"],
"ipv4": {
"address": [{
"ip": "192.168.2.1",
"prefix-length": 24
}]
}
},
]
}
},
"infix-dhcp-server": {
"dhcp-server": {
"option": [{
"id": "router", "address": "auto"
}],
"subnet": [
{
"subnet": "192.168.1.0/24",
"pool": {
"start-address": POOL1,
"end-address": POOL1
},
"host": [{
"address": ADDRESS1,
"match": {
"client-id": {"hex": HOSTCID1}
},
"option": [
{
"id": "hostname",
"name": HOSTNM11
}, {
"id": "classless-static-route",
"static-route": [{
"destination": "0.0.0.0/0",
"next-hop": GW1
}]
}
]
}]
}, {
"subnet": "192.168.2.0/24",
"pool": {
"start-address": POOL2,
"end-address": POOL2
},
"host": [{
"address": ADDRESS2,
"match": {
"client-id": {"str": HOSTCID2}
},
"option": [
{
"id": "hostname",
"name": HOSTNM22
}
],
"lease-time": "infinite"
}]
},
]
}
}})
# We request hostname option just to ensure we don't get it.
client1.put_config_dicts({
"ietf-system": {
"system": {"hostname": HOSTNM1}
},
"infix-dhcp-client": {
"dhcp-client": {
"client-if": [{
"if-name": client1["link"],
"option": [
{"id": "router"},
{"id": "client-id", "hex": HOSTCID1},
{"id": 121}
]
}]
}
},
})
client2.put_config_dicts({
"ietf-system": {
"system": {"hostname": HOSTNM2}
},
"infix-dhcp-client": {
"dhcp-client": {
"client-if": [{
"if-name": client2["link"],
"client-id": HOSTCID2,
"option": [
{"id": "router"},
{"id": "hostname"},
{"id": 121}
]
}]
}
},
})
with test.step("Verify DHCP client1 static lease"):
until(lambda: iface.address_exist(client1, client1["link"], ADDRESS1))
with test.step("Verify client1 hostname has *not* changed"):
until(lambda: client1.get_data("/ietf-system:system")["system"]["hostname"] == HOSTNM1)
with test.step("Verify DHCP client1 has default route via option 121"):
until(lambda: route.ipv4_route_exist(client1, "0.0.0.0/0", nexthop=GW1))
with test.step("Verify DHCP client2 static lease"):
until(lambda: iface.address_exist(client2, client2["link"], ADDRESS2))
with test.step("Verify client2 hostname has changed"):
until(lambda: client2.get_data("/ietf-system:system")["system"]["hostname"] == HOSTNM22)
with test.step("Verify DHCP client2 has default gateway"):
until(lambda: route.ipv4_route_exist(client2, "0.0.0.0/0", nexthop=GW2))
test.succeed()
@@ -0,0 +1,42 @@
graph "server hosts" {
layout="neato";
overlap="false";
esep="+40";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt1> mgmt1 | <mgmt0> mgmt0 | <mgmt2> mgmt2 }",
pos="0,12!",
requires="controller",
];
server [
label="{ <link1> link1 | <mgmt> mgmt | <link2> link2 } | server",
pos="15,12!",
requires="infix",
];
client1 [
label="{ <mgmt> mgmt | <link> link} | client1",
pos="15,18!",
requires="infix",
];
client2 [
label="{ <link> link | <mgmt> mgmt } | client2",
pos="15,6!",
requires="infix",
];
host:mgmt0 -- server:mgmt [requires="mgmt", color=lightgrey]
host:mgmt1 -- client1:mgmt [requires="mgmt", color=lightgrey]
host:mgmt2 -- client2:mgmt [requires="mgmt", color=lightgrey]
server:link1 -- client1:link [color=black, fontcolor=black, taillabel="192.168.1.1/24"]
server:link2 -- client2:link [color=black, fontcolor=black, taillabel="192.168.2.1/24"]
}
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: server hosts Pages: 1 -->
<svg width="520pt" height="372pt"
viewBox="0.00 0.00 520.29 372.03" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 368.03)">
<title>server hosts</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-368.03 516.29,-368.03 516.29,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-147.52 0,-216.52 108,-216.52 108,-147.52 0,-147.52"/>
<text text-anchor="middle" x="25" y="-178.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-147.52 50,-216.52 "/>
<text text-anchor="middle" x="79" y="-201.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
<polyline fill="none" stroke="black" points="50,-193.52 108,-193.52 "/>
<text text-anchor="middle" x="79" y="-178.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt0</text>
<polyline fill="none" stroke="black" points="50,-170.52 108,-170.52 "/>
<text text-anchor="middle" x="79" y="-155.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
</g>
<!-- server -->
<g id="node2" class="node">
<title>server</title>
<polygon fill="none" stroke="black" points="388.29,-147.52 388.29,-216.52 512.29,-216.52 512.29,-147.52 388.29,-147.52"/>
<text text-anchor="middle" x="417.29" y="-201.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">link1</text>
<polyline fill="none" stroke="black" points="388.29,-193.52 446.29,-193.52 "/>
<text text-anchor="middle" x="417.29" y="-178.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="388.29,-170.52 446.29,-170.52 "/>
<text text-anchor="middle" x="417.29" y="-155.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">link2</text>
<polyline fill="none" stroke="black" points="446.29,-147.52 446.29,-216.52 "/>
<text text-anchor="middle" x="479.29" y="-178.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">server</text>
</g>
<!-- host&#45;&#45;server -->
<g id="edge1" class="edge">
<title>host:mgmt0&#45;&#45;server:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-182.02C108,-182.02 388.29,-182.02 388.29,-182.02"/>
</g>
<!-- client1 -->
<g id="node3" class="node">
<title>client1</title>
<polygon fill="none" stroke="black" points="388.29,-317.53 388.29,-363.53 512.29,-363.53 512.29,-317.53 388.29,-317.53"/>
<text text-anchor="middle" x="413.29" y="-348.33" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="388.29,-340.53 438.29,-340.53 "/>
<text text-anchor="middle" x="413.29" y="-325.33" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="438.29,-317.53 438.29,-363.53 "/>
<text text-anchor="middle" x="475.29" y="-336.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">client1</text>
</g>
<!-- host&#45;&#45;client1 -->
<g id="edge2" class="edge">
<title>host:mgmt1&#45;&#45;client1:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-205.02C108,-205.02 388.29,-352.53 388.29,-352.53"/>
</g>
<!-- client2 -->
<g id="node4" class="node">
<title>client2</title>
<polygon fill="none" stroke="black" points="388.29,-0.5 388.29,-46.5 512.29,-46.5 512.29,-0.5 388.29,-0.5"/>
<text text-anchor="middle" x="413.29" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="388.29,-23.5 438.29,-23.5 "/>
<text text-anchor="middle" x="413.29" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="438.29,-0.5 438.29,-46.5 "/>
<text text-anchor="middle" x="475.29" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">client2</text>
</g>
<!-- host&#45;&#45;client2 -->
<g id="edge3" class="edge">
<title>host:mgmt2&#45;&#45;client2:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-159.02C108,-159.02 388.29,-11.5 388.29,-11.5"/>
</g>
<!-- server&#45;&#45;client1 -->
<g id="edge4" class="edge">
<title>server:link1&#45;&#45;client1:link</title>
<path fill="none" stroke="black" stroke-width="2" d="M417.29,-217.02C417.29,-217.02 413.29,-317.53 413.29,-317.53"/>
<text text-anchor="middle" x="362.79" y="-220.82" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.1/24</text>
</g>
<!-- server&#45;&#45;client2 -->
<g id="edge5" class="edge">
<title>server:link2&#45;&#45;client2:link</title>
<path fill="none" stroke="black" stroke-width="2" d="M417.29,-147.02C417.29,-147.02 413.29,-46.5 413.29,-46.5"/>
<text text-anchor="middle" x="362.79" y="-135.82" font-family="DejaVu Serif, Book" font-size="14.00">192.168.2.1/24</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

+1
View File
@@ -0,0 +1 @@
server_subnets.adoc
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 519 KiB

@@ -0,0 +1,59 @@
=== DHCP Server Multiple Subnets
==== Description
Verify that the DHCP server is capble of acting on more than one subnet,
handing out leases from a pool and static host lease, ensuring global,
subnet, and host-specific options are honored and do not leak between
subnets.
.Internal network setup, client2 and client3 are on the same LAN
ifdef::topdoc[]
image::../../test/case/infix_dhcp/server_subnets/dhcp-subnets.svg[Internal networks]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::server_subnets/dhcp-subnets.svg[Internal networks]
endif::testgroup[]
ifndef::testgroup[]
image::dhcp-subnets.svg[Internal networks]
endif::testgroup[]
endif::topdoc[]
To verify isolation of subnet settings, a few "decoys" are added to the
configuration of each subnet. These are then checked for on each of the
clients. E.g., both subnets have static host configurations, but only
one client should match.
Both DNS and NTP servers are handed out to clients. Some clients have
a static DNS and NTP server configured already.
The test is concluded by the server trying to reach each client using
ping of the hostname.
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_dhcp/server_subnets/topology.svg[DHCP Server Multiple Subnets topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::server_subnets/topology.svg[DHCP Server Multiple Subnets topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[DHCP Server Multiple Subnets topology]
endif::testgroup[]
endif::topdoc[]
==== Test sequence
. Set up topology and attach to client and server DUTs
. Configure DHCP server and clients
. Verify DHCP client1 get correct lease
. Verify DHCP client1 has default route via server
. Verify DHCP client1 has correct DNS server(s)
. Verify DHCP client2 get correct static lease
. Verify DHCP client2 has default route via classless-static-route
. Verify DHCP client2 has correct DNS and NTP server(s)
. Verify DHCP client3 get correct lease
. Verify DHCP client3 has default route via server
. Verify DHCP client3 has correct DNS and NTP server(s)
<<<
+313
View File
@@ -0,0 +1,313 @@
#!/usr/bin/env python3
"""DHCP Server Multiple Subnets
Verify that the DHCP server is capble of acting on more than one subnet,
handing out leases from a pool and static host lease, ensuring global,
subnet, and host-specific options are honored and do not leak between
subnets.
.Internal network setup, client2 and client3 are on the same LAN
image::dhcp-subnets.svg[Internal networks]
To verify isolation of subnet settings, a few "decoys" are added to the
configuration of each subnet. These are then checked for on each of the
clients. E.g., both subnets have static host configurations, but only
one client should match.
Both DNS and NTP servers are handed out to clients. Some clients have
a static DNS and NTP server configured already.
The test is concluded by the server trying to reach each client using
ping of the hostname.
"""
import infamy
import infamy.iface as iface
import infamy.route as route
from infamy.util import until
def has_dns_server(data, dns_servers):
"""Verify system have all the given DNS servers"""
servers = data.get('system-state', {}) \
.get('dns-resolver', {}) \
.get('server')
if not servers:
return False
configured = {server['address'] for server in servers}
return all(server in configured for server in dns_servers)
def has_ntp_server(data, ntp_server):
"""Verify system has, or does *not* have*, an NTP server"""
sources = data.get('system-state', {}) \
.get('ntp', {}) \
.get('sources', {}) \
.get('source')
if not ntp_server:
# This system should *not* have any NTP server
return not sources
if not sources:
return False
for source in sources:
if source['address'] == ntp_server:
return True
return False
def has_system_servers(dut, dns, ntp=None):
"""Verify DUT have all DNS and, if given, NTP server(s)"""
data = dut.get_data("/ietf-system:system-state")
if data is None:
return False
return has_dns_server(data, dns) and has_ntp_server(data, ntp)
with infamy.Test() as test:
SERVER1 = '192.168.1.1'
POOL1 = '192.168.1.100'
ADDR1 = '192.168.1.11'
SERVER2 = '192.168.2.1'
POOL2 = '192.168.2.200'
ADDR2 = '192.168.2.22'
GW1 = '192.168.1.11'
GW2 = '192.168.2.2'
HOSTNM1 = 'client1'
HOSTNM2 = 'client2'
HOSTNM3 = 'client3'
with test.step("Set up topology and attach to client and server DUTs"):
env = infamy.Env()
server = env.attach("server", "mgmt")
client1 = env.attach("client1", "mgmt")
client2 = env.attach("client2", "mgmt")
client3 = env.attach("client3", "mgmt")
with test.step("Configure DHCP server and clients"):
server.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [
{
"name": server["client1"],
"ipv4": {
"address": [{
"ip": SERVER1,
"prefix-length": 24
}]
}
}, {
"name": "br0",
"type": "infix-if-type:bridge",
"ipv4": {
"address": [{
"ip": SERVER2,
"prefix-length": 24,
}]
}
}, {
"name": server["client2"],
"infix-interfaces:bridge-port": {
"bridge": "br0"
}
}, {
"name": server["client3"],
"infix-interfaces:bridge-port": {
"bridge": "br0"
}
},
]
}
},
"infix-dhcp-server": {
"dhcp-server": {
"option": [
{
"id": "dns-server",
"address": "auto"
},
],
# No client should get the static host lease on this
# subnet. Only a pool address and global options.
"subnet": [
{
"subnet": "192.168.1.0/24",
"option": [{
"id": "router",
"address": "auto"
}],
"pool": {
"start-address": POOL1,
"end-address": POOL1
},
# Decoy, client2 should not get this lease!
"host": [{
"address": ADDR1,
"match": {
"hostname": HOSTNM2
},
"option": [{
"id": "classless-static-route",
"static-route": [{
"destination": "0.0.0.0/0",
"next-hop": GW1
}]
}]
}]
},
{
"subnet": "192.168.2.0/24",
"option": [
{
"id": "ntp-server",
"address": "auto"
}, {
# Verify correct option selection in
# client: option 3 < option 121
"id": "router",
"address": "auto"
}
],
"pool": {
"start-address": POOL2,
"end-address": POOL2
},
"host": [{
"address": ADDR2,
"match": {
"hostname": HOSTNM2
},
"option": [{
"id": "classless-static-route",
"static-route": [{
"destination": "0.0.0.0/0",
"next-hop": GW2
}]
}]
}]
},
]
}
}})
# All clients request/accept the same options. We do this to
# both keep fleet configuration simple but also to verify that
# the server is behaving correctly.
client1.put_config_dicts({
"infix-dhcp-client": {
"dhcp-client": {
"client-if": [{
"if-name": client1["server"],
"option": [
{"id": "hostname", "value": "auto"},
{"id": "router"},
{"id": "dns-server"},
{"id": "ntp-server"},
{"id": 121}
]
}]
}
},
"ietf-system": {
"system": {
"hostname": HOSTNM1,
"ntp": {"enabled": True},
}
}})
client2.put_config_dicts({
"infix-dhcp-client": {
"dhcp-client": {
"client-if": [{
"if-name": client2["server"],
"option": [
{"id": "hostname", "value": "auto"},
{"id": "router"},
{"id": "dns-server"},
{"id": "ntp-server"},
{"id": 121}
]
}]
}
},
"ietf-system": {
"system": {
"hostname": HOSTNM2,
"ntp": {"enabled": True},
}
}})
client3.put_config_dicts({
"infix-dhcp-client": {
"dhcp-client": {
"client-if": [{
"if-name": client3["server"],
"option": [
{"id": "hostname", "value": "auto"},
{"id": "router"},
{"id": "dns-server"},
{"id": "ntp-server"},
{"id": 121}
]
}]
}
},
"ietf-system": {
"system": {
"hostname": HOSTNM3,
"ntp": {"enabled": True},
"dns-resolver": {
"search": [
"example.com",
"kernelkit.org"
],
"server": [
{
"name": "static",
"udp-and-tcp": {
"address": "1.2.3.4"
}
}
],
"options": {
"timeout": 3,
"attempts": 5
}
},
}
}})
with test.step("Verify DHCP client1 get correct lease"):
until(lambda: iface.address_exist(client1, client1["server"], POOL1))
with test.step("Verify DHCP client1 has default route via server"):
until(lambda: route.ipv4_route_exist(client1, "0.0.0.0/0", nexthop=SERVER1))
with test.step("Verify DHCP client1 has correct DNS server(s)"):
until(lambda: has_system_servers(client1, ["192.168.1.1"]))
with test.step("Verify DHCP client2 get correct static lease"):
until(lambda: iface.address_exist(client2, client2["server"], ADDR2))
with test.step("Verify DHCP client2 has default route via classless-static-route"):
until(lambda: route.ipv4_route_exist(client2, "0.0.0.0/0", nexthop=GW2))
with test.step("Verify DHCP client2 has correct DNS and NTP server(s)"):
until(lambda: has_system_servers(client2, ["192.168.2.1"], "192.168.2.1"))
with test.step("Verify DHCP client3 get correct lease"):
until(lambda: iface.address_exist(client3, client3["server"], POOL2))
with test.step("Verify DHCP client3 has default route via server"):
until(lambda: route.ipv4_route_exist(client3, "0.0.0.0/0", nexthop=SERVER2))
with test.step("Verify DHCP client3 has correct DNS and NTP server(s)"):
until(lambda: has_system_servers(client3, ["1.2.3.4", "192.168.2.1"], "192.168.2.1"))
test.succeed()
@@ -0,0 +1,51 @@
graph "server subnets" {
layout="neato";
overlap="false";
esep="+40";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <client1> client1 | <server> server | <client3> client3 | <client2> client2 }",
pos="0,120!",
requires="controller",
];
client1 [
label="{ <mgmt> mgmt | <server> server} | client1",
pos="20,130!",
requires="infix",
];
server [
label="{ <client1> client1 | <mgmt> mgmt | { <client2> client2 | <client3> client3 } } | server&nbsp;",
pos="20,120!",
requires="infix",
];
client2 [
label="{ <server> server | <mgmt> mgmt } | client2",
pos="10,110!",
requires="infix",
];
client3 [
label="{ <server> server | <mgmt> mgmt } | client3",
pos="30,110!",
requires="infix",
];
host:server -- server:mgmt [requires="mgmt", color=lightgrey]
host:client1 -- client1:mgmt [requires="mgmt", color=lightgrey]
host:client2 -- client2:mgmt [requires="mgmt", color=lightgrey]
host:client3 -- client3:mgmt [requires="mgmt", color=lightgrey]
server:client1 -- client1:server [color=blue, fontcolor=black, taillabel="192.168.1.1/24"]
server:client2 -- client2:server [color=green]
server:client3 -- client3:server [color=green, fontcolor=black, taillabel="192.168.2.1/24"]
}
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: server subnets Pages: 1 -->
<svg width="650pt" height="395pt"
viewBox="0.00 0.00 650.05 395.03" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 391.03)">
<title>server subnets</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-391.03 646.05,-391.03 646.05,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-147.52 0,-239.52 124,-239.52 124,-147.52 0,-147.52"/>
<text text-anchor="middle" x="25" y="-189.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-147.52 50,-239.52 "/>
<text text-anchor="middle" x="87" y="-224.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">client1</text>
<polyline fill="none" stroke="black" points="50,-216.52 124,-216.52 "/>
<text text-anchor="middle" x="87" y="-201.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">server</text>
<polyline fill="none" stroke="black" points="50,-193.52 124,-193.52 "/>
<text text-anchor="middle" x="87" y="-178.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">client3</text>
<polyline fill="none" stroke="black" points="50,-170.52 124,-170.52 "/>
<text text-anchor="middle" x="87" y="-155.32" font-family="DejaVu Sans Mono, Book" font-size="14.00">client2</text>
</g>
<!-- client1 -->
<g id="node2" class="node">
<title>client1</title>
<polygon fill="none" stroke="black" points="332.03,-340.53 332.03,-386.53 472.03,-386.53 472.03,-340.53 332.03,-340.53"/>
<text text-anchor="middle" x="365.03" y="-371.33" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="332.03,-363.53 398.03,-363.53 "/>
<text text-anchor="middle" x="365.03" y="-348.33" font-family="DejaVu Sans Mono, Book" font-size="14.00">server</text>
<polyline fill="none" stroke="black" points="398.03,-340.53 398.03,-386.53 "/>
<text text-anchor="middle" x="435.03" y="-359.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">client1</text>
</g>
<!-- host&#45;&#45;client1 -->
<g id="edge2" class="edge">
<title>host:client1&#45;&#45;client1:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M124,-228.52C124,-228.52 332.03,-375.53 332.03,-375.53"/>
</g>
<!-- server -->
<g id="node3" class="node">
<title>server</title>
<polygon fill="none" stroke="black" points="291.03,-159.02 291.03,-228.02 513.03,-228.02 513.03,-159.02 291.03,-159.02"/>
<text text-anchor="middle" x="365.03" y="-212.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">client1</text>
<polyline fill="none" stroke="black" points="291.03,-205.02 439.03,-205.02 "/>
<text text-anchor="middle" x="365.03" y="-189.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="291.03,-182.02 439.03,-182.02 "/>
<text text-anchor="middle" x="328.03" y="-166.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">client2</text>
<polyline fill="none" stroke="black" points="365.03,-159.02 365.03,-182.02 "/>
<text text-anchor="middle" x="402.03" y="-166.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">client3</text>
<polyline fill="none" stroke="black" points="439.03,-159.02 439.03,-228.02 "/>
<text text-anchor="middle" x="476.03" y="-189.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">server </text>
</g>
<!-- host&#45;&#45;server -->
<g id="edge1" class="edge">
<title>host:server&#45;&#45;server:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M124,-205.52C124,-205.52 291.03,-193.52 291.03,-193.52"/>
</g>
<!-- client2 -->
<g id="node4" class="node">
<title>client2</title>
<polygon fill="none" stroke="black" points="162.02,-0.5 162.02,-46.5 302.02,-46.5 302.02,-0.5 162.02,-0.5"/>
<text text-anchor="middle" x="195.02" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">server</text>
<polyline fill="none" stroke="black" points="162.02,-23.5 228.02,-23.5 "/>
<text text-anchor="middle" x="195.02" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="228.02,-0.5 228.02,-46.5 "/>
<text text-anchor="middle" x="265.02" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">client2</text>
</g>
<!-- host&#45;&#45;client2 -->
<g id="edge3" class="edge">
<title>host:client2&#45;&#45;client2:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M124,-158.52C124,-158.52 162.02,-11.5 162.02,-11.5"/>
</g>
<!-- client3 -->
<g id="node5" class="node">
<title>client3</title>
<polygon fill="none" stroke="black" points="502.05,-0.5 502.05,-46.5 642.05,-46.5 642.05,-0.5 502.05,-0.5"/>
<text text-anchor="middle" x="535.05" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">server</text>
<polyline fill="none" stroke="black" points="502.05,-23.5 568.05,-23.5 "/>
<text text-anchor="middle" x="535.05" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="568.05,-0.5 568.05,-46.5 "/>
<text text-anchor="middle" x="605.05" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">client3</text>
</g>
<!-- host&#45;&#45;client3 -->
<g id="edge4" class="edge">
<title>host:client3&#45;&#45;client3:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M124,-181.52C124,-181.52 502.05,-11.5 502.05,-11.5"/>
</g>
<!-- server&#45;&#45;client1 -->
<g id="edge5" class="edge">
<title>server:client1&#45;&#45;client1:server</title>
<path fill="none" stroke="blue" stroke-width="2" d="M365.03,-228.52C365.03,-228.52 365.03,-340.53 365.03,-340.53"/>
<text text-anchor="middle" x="310.53" y="-232.32" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.1/24</text>
</g>
<!-- server&#45;&#45;client2 -->
<g id="edge6" class="edge">
<title>server:client2&#45;&#45;client2:server</title>
<path fill="none" stroke="green" stroke-width="2" d="M291.03,-170.52C291.03,-170.52 195.02,-46.5 195.02,-46.5"/>
</g>
<!-- server&#45;&#45;client3 -->
<g id="edge7" class="edge">
<title>server:client3&#45;&#45;client3:server</title>
<path fill="none" stroke="green" stroke-width="2" d="M402.03,-158.52C402.03,-158.52 502.05,-35.5 502.05,-35.5"/>
<text text-anchor="middle" x="347.53" y="-147.32" font-family="DejaVu Serif, Book" font-size="14.00">192.168.2.1/24</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

+21
View File
@@ -93,6 +93,27 @@
}
]
}
},
"infix-system:dns-resolver": {
"options": {
"timeout": 3,
"attempts": 5
},
"server": [
{
"address": "1.2.3.4",
"origin": "static"
},
{
"address": "192.168.2.1",
"origin": "dhcp",
"interface": "e7"
}
],
"search": [
"example.com",
"kernelkit.org"
]
}
}
}
@@ -0,0 +1,3 @@
options timeout:3 attempts:5
search example.com kernelkit.org
nameserver 1.2.3.4
@@ -0,0 +1,3 @@
# resolv.conf from e7.conf
nameserver 192.168.2.1 # e7
+8 -6
View File
@@ -138,10 +138,10 @@ class TestCase:
parsed_script = ast.parse(script_content)
visitor = TestStepVisitor()
visitor.visit(parsed_script)
description=visitor.description if visitor.description != "" else "Undefined"
test_steps=visitor.test_steps
description = visitor.description if visitor.description != "" else "Undefined"
test_steps = visitor.test_steps
if variables is not None:
for k,v in variables.items():
for k, v in variables.items():
if "{" + k + "}" in description:
description = description.replace("{" + k + "}", v)
@@ -205,11 +205,13 @@ def parse_suite(directory, root, suitefile):
test_case.generate_specification(test_title, test_file, test_spec, variables)
readme.write(f"include::{path}{test_case_id}.adoc[]\n\n")
if path != "":
lnk = f"{directory}/{path}/Readme.adoc"
readme.close()
os.unlink(f"{directory}/{path}/Readme.adoc.tmp")
readme = None
os.unlink(f"{directory}/{path}/Readme.adoc")
os.symlink(f"{test_case_id}.adoc", f"{directory}/{path}/Readme.adoc")
if os.path.exists(lnk):
os.unlink(lnk)
os.symlink(f"{test_case_id}.adoc", lnk)
if readme is not None:
readme.close()
os.rename(f"{directory}/Readme.adoc.tmp", f"{directory}/Readme.adoc")
@@ -219,7 +221,7 @@ parser = argparse.ArgumentParser(description="Generate a test specification for
parser.add_argument("-s", "--suite", required=True, help="The suite file to parse")
parser.add_argument("-r", "--root-dir", required=True, help="Path that all paths should be relative to")
parser.add_argument("-d", "--debug", help="Show debug output", action="store_true")
args=parser.parse_args()
args = parser.parse_args()
error_string = ""
# This is hacky, graphviz output error only to stdout and return successful(always).
+7 -2
View File
@@ -22,7 +22,7 @@ commands:
modify MOD Modify (change/update) module, may use :ALL
install MOD Install module
uninstall MOD Uninstall module (force)
update PATH Update module from PATH
EOF
}
@@ -99,7 +99,12 @@ case $cmd in
$CTL -c "$1" $perms $owner $group $features -v3
;;
uninstall)
$CTL -u "$1" -f -v3
for mod in "$@"; do
$CTL -u "$mod" -f -v3
done
;;
update)
$CTL -U "$1" -s "$MOD" $perms $owner $group $features -v3
;;
*)
echo "NOP"