Merge pull request #887 from kernelkit/dhcp-server
Add basic DHCP server support
@@ -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
|
||||
|
||||
|
||||
@@ -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 $*
|
||||
;;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
])
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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,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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
infix-dhcp-common.yang
|
||||
@@ -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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
infix-dhcp-server.yang
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")))
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
<<<
|
||||
|
||||
@@ -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--server -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt2--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--client -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--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--client -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>server:link--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 |
@@ -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
|
||||
|
||||
|
||||
<<<
|
||||
|
||||
@@ -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--server -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt0--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--client1 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt1--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--client2 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>host:mgmt2--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--client1 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>server:link1--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--client2 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>server:link2--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 |
@@ -0,0 +1 @@
|
||||
server_subnets.adoc
|
||||
|
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)
|
||||
|
||||
|
||||
<<<
|
||||
|
||||
@@ -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 ",
|
||||
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--client1 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:client1--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--server -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:server--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--client2 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>host:client2--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--client3 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>host:client3--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--client1 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>server:client1--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--client2 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>server:client2--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--client3 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>server:client3--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 |
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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"
|
||||
|
||||