confd: reduce dhcp-server to minimal scope

- dnsmasq:
   - allow listening to all interfaces again to serve DHCP leases
   - disable most of the default DHCP options, keep broadcast and
     netmask because when dnsmasq can infer these they are better
     than breaking networking for clients -- note, they can still
     be overridden from global/subnet/host scope -- and for relay
     setups they need to be supplied anyway
 - confd:
   - drop per-interface dnsmasq, although practical for many use-cases,
     having a per-subnet focused server means improved integration with
     future stand-alone dhcp relay setup
   - implement suggested, simplified, yang model

Fixes #703

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2025-01-31 13:56:28 +01:00
parent b196194e5f
commit 3c1e9f3199
13 changed files with 943 additions and 891 deletions
+9 -6
View File
@@ -3,16 +3,19 @@
# and similar events feed servers and configuration to dnsmasq.
domain-needed
# Only listen to loopback (local system)
interface=lo
bind-dynamic
#listen-address=127.0.0.1,::1
# Allow configuration and cache clear over D-Bus
enable-dbus
# Disable the following dnsmasq default DHCP options
#dhcp-option=option:netmask
#dhcp-option=28 # option:broadcast
#dhcp-option=option:domain-name
dhcp-option=option:router
dhcp-option=option:dns-server
dhcp-option=12 # option:hostname
# Generated by openresolv
resolv-file=/var/lib/misc/resolv.conf
# Include all files in a directory which end in .conf
conf-dir=/etc/dnsmasq.d/,*.conf
-1
View File
@@ -11,7 +11,6 @@ define PYTHON_STATD_MOVE_BINARIES
mv $(TARGET_DIR)/usr/bin/yanger $(TARGET_DIR)/usr/libexec/statd/
mv $(TARGET_DIR)/usr/bin/cli-pretty $(TARGET_DIR)/usr/libexec/statd/
mv $(TARGET_DIR)/usr/bin/ospf-status $(TARGET_DIR)/usr/libexec/statd/
mv $(TARGET_DIR)/usr/bin/dhcp-server-status $(TARGET_DIR)/usr/libexec/statd/
endef
PYTHON_STATD_POST_INSTALL_TARGET_HOOKS += PYTHON_STATD_MOVE_BINARIES
$(eval $(python-package))
@@ -1 +1 @@
service [S12345] <pid/syslogd> dnsmasq -k -u root -- DHCP/DNS proxy
service [S12345] <!pid/syslogd> dnsmasq -k -u root -- DHCP/DNS proxy
+328 -391
View File
@@ -10,28 +10,31 @@
#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 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 */
#define DEFAULT_LEASETIME 300 /* seconds */
#define MAX_LEASE_COUNT 1024 /* max. number of leases */
#define MAX_RELAY_SERVER 2 /* max. number of relay servers */
struct option {
TAILQ_ENTRY(option) list;
int num;
char name[32];
TAILQ_ENTRY(option) list;
int num;
char name[32];
};
TAILQ_HEAD(options, option);
static struct options known_options;
static char * strip(char *line)
static char *strip(char *line)
{
char *p;
@@ -49,49 +52,6 @@ static char * strip(char *line)
return line;
}
static int ip_address_is_valid (const char *ifname, const char *str)
{
struct ifaddrs *ifaddr, *ifa;
struct sockaddr_in *sin;
struct sockaddr_in6 *sin6;
struct in_addr addr4;
struct in6_addr addr6;
int fam, ret = 0;
if (inet_pton(AF_INET, str, &addr4) == 1)
fam = AF_INET;
else if (inet_pton(AF_INET6, str, &addr6) == 1)
fam = AF_INET6;
else
return 0;
if (getifaddrs(&ifaddr) < 0)
return 0;
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
if (!ifa->ifa_addr ||
strcmp(ifa->ifa_name, ifname) != 0 ||
ifa->ifa_addr->sa_family != fam)
continue;
if (fam == AF_INET) {
sin = (struct sockaddr_in *) ifa->ifa_addr;
if (sin->sin_addr.s_addr == addr4.s_addr) {
ret = 1;
break;
}
} else if (fam == AF_INET6) {
sin6 = (struct sockaddr_in6 *) ifa->ifa_addr;
if (memcmp(sin6->sin6_addr.s6_addr,
addr6.s6_addr, 16) == 0) {
ret = 1;
break;
}
}
}
freeifaddrs(ifaddr);
return ret;
}
static void add_known_option(char *line)
{
@@ -117,34 +77,7 @@ static void add_known_option(char *line)
}
}
static int load_known_options(void)
{
char line[256];
FILE *pp;
TAILQ_INIT(&known_options);
/*
* From dnsmasq.8:
*
* --help dhcp will display known DHCPv4 configuration options
* --help dhcp6 will display DHCPv6 options
*/
pp = popen("dnsmasq --help dhcp", "r");
if (!pp) {
ERROR("Unable to retrieve known options");
return -1;
}
while (!feof(pp)) {
if (fgets(line, sizeof(line), pp))
add_known_option(strip(line));
}
pclose(pp);
return 0;
}
static struct option * get_known_option(const char *name)
static struct option *get_known_option(const char *name)
{
struct option *opt;
int num = -1;
@@ -167,348 +100,302 @@ static struct option * get_known_option(const char *name)
return NULL;
}
/* configure dnsmasq options */
static int configure_options(FILE *fp, struct lyd_node *cfg)
static int load_known_options(void)
{
struct lyd_node *option;
struct option *opt;
const char *value, *name;
char line[256];
FILE *pp;
LYX_LIST_FOR_EACH(lyd_child(cfg), option, "option") {
value = lydx_get_cattr(option, "value");
name = lydx_get_cattr(option, "name");
TAILQ_INIT(&known_options);
if (!value || !name)
continue;
/*
* From dnsmasq.8:
*
* --help dhcp will display known DHCPv4 configuration options
* --help dhcp6 will display DHCPv6 options
*/
pp = popen("dnsmasq --help dhcp", "r");
if (!pp) {
ERROR("Unable to retrieve dnsmasq DHCP options");
return -1;
}
while (!feof(pp)) {
if (fgets(line, sizeof(line), pp))
add_known_option(strip(line));
}
pclose(pp);
opt = get_known_option(name);
if (!opt) {
ERROR("Unknown option %s", name);
return -1;
}
fprintf(fp, "dhcp-option=%d,%s\n", opt->num, value);
if (!get_known_option("hostname")) {
snprintf(line, sizeof(line), "12 hostname\n");
add_known_option(strip(line));
}
return 0;
}
static int configure_host(FILE *fp, struct lyd_node *host, int leasetime)
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;
}
/* configure dnsmasq options */
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") {
const char *id = lydx_get_cattr(option, "id");
struct lyd_node *suboption;
const struct option *opt;
const char *val;
opt = get_known_option(id);
if (!opt) {
ERROR("Unknown option %s", 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")
?: 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 ? "," : "", opt->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 ? "," : "", opt->num,
lydx_get_cattr(net, "destination"),
lydx_get_cattr(net, "next-hop"));
}
} else {
ERROR("Unknown value to option %s", id);
return -1;
}
}
return 0;
}
static const char *host_match(struct lyd_node *match, const char **id)
{
struct lyd_node *node;
const char *ip, *str, *pfx;
size_t i;
struct {
const char *node;
const char *key;
const char *prefix;
} entries[] = {
{ "mac-address", NULL },
{ "hostname", NULL },
{ "client-identifier", "id:" }
} choice[] = {
{ "mac-address", NULL },
{ "hostname", NULL },
{ "client-id", "id:" }
};
node = lydx_get_child(host, "ip-address");
if (!node)
return -1;
ip = lyd_get_value(node);
if (!ip || !*ip)
return -1;
if (!match)
return NULL;
for (i = 0; i < sizeof(entries)/sizeof(entries[0]); i++) {
node = lydx_get_child(host, entries[i].node);
for (size_t i = 0; i < NELEMS(choice); i++) {
struct lyd_node *node;
node = lydx_get_child(match, choice[i].key);
if (!node)
continue;
str = lyd_get_value(node);
if (str && *str) {
pfx = entries[i].prefix;
fprintf(fp, "dhcp-host=%s%s,%s,%d\n",
pfx ? pfx : "",
ip, str, leasetime);
return 0;
}
*id = choice[i].prefix;
return lyd_get_value(node);
}
return -1;
return NULL;
}
static int configure_pool(FILE *fp, struct lyd_node *pool, int leasetime)
/* 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)
{
struct lyd_node *node, *host;
const char *str, *first, *last;
const char *ip = lydx_get_cattr(host, "address");
const char *tag = host_tag(NULL, ip);
const char *match, *id, *name;
if ((str = lydx_get_cattr(pool, "pool-name")))
fprintf(fp, "# pool %s\n", str);
if ((str = lydx_get_cattr(pool, "lease-time")))
leasetime = atoi(str);
if ((node = lydx_get_child(pool, "first-ip-address")))
first = lyd_get_value(node);
else
match = host_match(lydx_get_child(host, "match"), &id);
if (!match)
return -1;
if ((node = lydx_get_child(pool, "last-ip-address")))
last = lyd_get_value(node);
else
fprintf(fp, "\n# Host specific options\n");
if (configure_options(fp, host, tag))
return -1;
fprintf(fp, "dhcp-range=%s,%s,%d\n", first, last, leasetime);
LYX_LIST_FOR_EACH(lyd_child(pool), host, "static-allocation") {
if (configure_host(fp, host, leasetime))
return -1;
}
return 0;
}
static int configure_server(FILE *fp, const char *ifname, struct lyd_node *cfg)
{
struct lyd_node *pool;
int leasetime = DEFAULT_LEASETIME;
const char *str;
if (configure_options(fp, cfg))
return -1;
if ((str = lydx_get_cattr(cfg, "lease-time")))
leasetime = atoi(str);
LYX_LIST_FOR_EACH(lyd_child(cfg), pool, "ip-pool") {
fprintf(fp, "\n");
if (configure_pool(fp, pool, leasetime))
return -1;
}
return 0;
}
static int configure_relay_info (FILE *fp, struct lyd_node *cfg, const char *id)
{
struct lyd_node *node;
const char *str, *p;
int count = 0;
name = lydx_get_cattr(host, "hostname");
/*
* Here we configure identifiers for DHCP option 82 (Relay Agent Information)
* which get inserted into relayed DHCP requests (see RFC3046).
* The DHCP option space is quite limited, thus we only allow 4 bytes for each
* identifier.
*
* If circuit-id is not set, dnsmasq will use the interface index instead.
*
* (see patches/dnsmasq/2.90/0000-relay-agent-info.patch)
* set host-specific tag, allow options from that tag,
* also allow options from subnet
*/
node = lydx_get_child(cfg, id);
if (!node)
return 0;
str = lyd_get_value(node);
for (p = str; p && *p; p++)
if (*p == ':') count++;
if (count == 0)
return 0;
if (count > 3) {
ERROR("Overflow in %s (max. 4 bytes allowed)", id);
return -1;
}
fprintf(fp, "dhcp-%s=set:enterprise,%s\n",
(strcmp(id, "circuit-id") == 0) ? "circuitid" : "remoteid",
str);
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 int configure_relay(FILE *fp, const char *ifname, struct lyd_node *cfg)
static void add(const char *subnet, struct lyd_node *cfg)
{
const char *tag = subnet_tag(subnet);
const char *val, *ifname;
struct lyd_node *node;
const char *addr, *str;
int count = 0;
if ((node = lydx_get_child(cfg, "address")))
addr = lyd_get_value(node);
if (!addr || !*addr) {
ERROR("Need relay address");
return -1;
}
if (!ip_address_is_valid(ifname, addr)) {
ERROR("Invalid relay address");
return -1;
}
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "server") {
if (count >= MAX_RELAY_SERVER) {
ERROR("Too many relay servers configured");
return -1;
}
if ((str = lydx_get_cattr(node, "ip-address"))) {
fprintf(fp, "dhcp-relay=%s,%s\n", addr, str);
count++;
}
}
if (count == 0) {
ERROR("No relay server configured");
return -1;
}
if (configure_relay_info(fp, cfg, "circuit-id") < 0 ||
configure_relay_info(fp, cfg, "remote-id") < 0)
return -1;
return 0;
}
static int configure_dnsmasq(const char *ifname, struct lyd_node *cfg)
{
struct lyd_node *node;
const char *mode = NULL;
const char *addr;
FILE *fp = NULL;
int ret;
int rc = 0;
fp = fopenf("w", "/var/run/dnsmasq-%s.conf", ifname);
fp = fopenf("w", DNSMASQ_SUBNET_FMT, tag);
if (!fp) {
ERROR("Failed to create dnsmasq conf for %s: %s", ifname, strerror(errno));
return -1;
ERROR("Failed creating dnsmasq conf for %s: %s", subnet, strerror(errno));
return;
}
fprintf(fp, "# Generated by Infix confd\n");
fprintf(fp, "pid-file=/var/run/dnsmasq-%s.pid\n", ifname);
fprintf(fp, "enable-dbus=%s.%s\n", DBUS_NAME_DNSMASQ, ifname);
fprintf(fp, "bind-interfaces\n");
fprintf(fp, "interface=%s\n", ifname);
fprintf(fp, "except-interface=lo\n");
fprintf(fp, "dhcp-leasefile=/var/run/dnsmasq-%s.leases\n", ifname);
fprintf(fp, "dhcp-lease-max=%d\n", MAX_LEASE_COUNT);
val = lydx_get_cattr(cfg, "description");
fprintf(fp, "# Subnet %s%s%s\n", subnet, val ? " - " : "", val ?: "");
if (debug)
fprintf(fp, "log-dhcp\n");
if ((node = lydx_get_child(cfg, "address"))) {
addr = lyd_get_value(node);
if (!ip_address_is_valid(ifname, addr)) {
ERROR("Invalid server address");
return -1;
}
fprintf(fp, "listen-address=%s\n", addr);
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;
}
if (!lydx_is_enabled(cfg, "dns-enabled")) {
fprintf(fp, "port=0\n");
fprintf(fp, "no-poll\n");
/* 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"));
}
fprintf(fp, "\n");
if ((node = lydx_get_child(cfg, "mode")))
mode = lyd_get_value(node);
err:
fclose(fp);
}
if (mode && strcmp(mode, "relay") == 0) {
INFO("Configuring DHCP relay on %s", ifname);
ret = configure_relay(fp, ifname,
lydx_get_child(cfg, "relay"));
} else {
INFO("Configuring DHCP server on %s", ifname);
ret = configure_server(fp, ifname,
lydx_get_child(cfg, "server"));
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);
if (ret != 0)
fremove("/var/run/dnsmasq-%s.conf", ifname);
return ret;
}
static int configure_finit(const char *ifname)
{
FILE *fp;
fp = fopenf("w", "/etc/finit.d/available/dhcp-server-%s.conf", ifname);
if (!fp) {
ERROR("Failed to create finit conf for %s: %s", ifname, strerror(errno));
return -1;
}
fprintf(fp, "# Generated by Infix confd\n");
fprintf(fp, "service <!> name:dhcp-server :%s <net/%s/running> \\\n"
" [2345] dnsmasq -k -u root -C /var/run/dnsmasq-%s.conf \\\n"
" -- DHCP server @%s\n",
ifname, ifname, ifname, ifname);
fclose(fp);
return 0;
}
static int configure_dbus(const char *ifname)
{
FILE *fp;
if (fexistf("/etc/dbus-1/system.d/dnsmasq-%s.conf", ifname)) {
return 0;
}
fp = fopenf("w", "/etc/dbus-1/system.d/dnsmasq-%s.conf", ifname);
if (!fp) {
ERROR("Failed to create dbus conf for %s: %s", ifname, strerror(errno));
return -1;
}
fprintf(fp, "<!DOCTYPE busconfig PUBLIC\n");
fprintf(fp, " \"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN\"\n");
fprintf(fp, " \"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd\">\n");
fprintf(fp, "<busconfig>\n");
fprintf(fp, " <policy user=\"root\">\n");
fprintf(fp, " <allow own=\"%s.%s\"/>\n", DBUS_NAME_DNSMASQ, ifname);
fprintf(fp, " <allow send_destination=\"%s.%s\"/>\n", DBUS_NAME_DNSMASQ, ifname);
fprintf(fp, " </policy>\n");
fprintf(fp, " <policy context=\"default\">\n");
fprintf(fp, " <deny own=\"%s.%s\"/>\n", DBUS_NAME_DNSMASQ, ifname);
fprintf(fp, " <deny send_destination=\"%s.%s\"/>\n", DBUS_NAME_DNSMASQ, ifname);
fprintf(fp, " </policy>\n");
fprintf(fp, "</busconfig>\n");
fclose(fp);
return 0;
}
static void add(const char *ifname, struct lyd_node *cfg)
{
const char *action = "disable";
if (configure_dnsmasq(ifname, cfg))
goto out;
if (configure_finit(ifname))
goto out;
if (configure_dbus(ifname))
goto out;
action = "enable";
out:
systemf("initctl -bfqn %s dhcp-server-%s", action, ifname);
}
static void del(const char *ifname)
{
systemf("initctl -bfq delete dhcp-server-%s", ifname);
/* 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;
const char *ifname, *cifname;
int enabled = 0, added = 0, deleted = 0;
sr_error_t err = 0;
sr_data_t *cfg;
int enabled = 0;
switch (event) {
case SR_EV_DONE:
@@ -519,7 +406,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, CFG_XPATH "//.", 0, 0, 0, &cfg);
err = sr_get_data(session, "//.", 0, 0, 0, &cfg);
if (err || !cfg)
goto err_abandon;
@@ -530,65 +417,114 @@ static int change(sr_session_ctx_t *session, uint32_t sub_id, const char *module
global = lydx_get_descendant(cfg->tree, "dhcp-server", NULL);
enabled = lydx_is_enabled(global, "enabled");
cifs = lydx_get_descendant(cfg->tree, "dhcp-server", "server-if", NULL);
difs = lydx_get_descendant(diff, "dhcp-server", "server-if", NULL);
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, "server-if") {
ifname = lydx_get_cattr(dif, "if-name");
if (!ifname)
continue;
LYX_LIST_FOR_EACH(difs, dif, "subnet") {
const char *subnet = lydx_get_cattr(dif, "subnet");
if (lydx_get_op(dif) == LYDX_OP_DELETE) {
del(ifname);
del(subnet, dif), deleted++;
continue;
}
LYX_LIST_FOR_EACH(cifs, cif, "server-if") {
cifname = lydx_get_cattr(cif, "if-name");
if (!cifname || strcmp(ifname, cifname))
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(ifname);
del(subnet, cif), deleted++;
else
add(ifname, cif);
add(subnet, cif), added++;
break;
}
}
if (!enabled) {
LYX_LIST_FOR_EACH(cifs, cif, "server-if") {
ifname = lydx_get_cattr(cif, "if-name");
if (ifname) {
INFO("DHCP server globally disabled, stopping server on %s", ifname);
del(ifname);
}
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 cleanstats(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)
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)
{
int rc;
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 (input_cnt < 1) {
ERROR("Not enough input parameters");
return SR_ERR_SYS;
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]);
}
rc = systemf("/usr/libexec/statd/dhcp-server-status --clean %s",
input[0].data.string_val);
return 0;
}
return (rc == 0) ? SR_ERR_OK : SR_ERR_SYS;
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)
@@ -600,7 +536,8 @@ int infix_dhcp_server_init(struct confd *confd)
goto fail;
REGISTER_CHANGE(confd->session, MODULE, CFG_XPATH, 0, change, confd, &confd->sub);
REGISTER_RPC(confd->session, ROOT_XPATH "clean-statistics", cleanstats, NULL, &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:
+1 -1
View File
@@ -31,7 +31,7 @@ MODULES=(
"ieee802-dot1ab-lldp@2022-03-15.yang"
"infix-lldp@2025-01-08.yang"
"infix-dhcp-client@2025-01-20.yang"
"infix-dhcp-server@2024-08-18.yang"
"infix-dhcp-server@2025-01-13.yang"
"infix-meta@2024-10-18.yang"
"infix-system@2024-11-27.yang"
"infix-services@2024-12-03.yang"
+409
View File
@@ -0,0 +1,409 @@
module infix-dhcp-server {
yang-version 1.1;
namespace "urn:infix:params:xml:ns:yang:infix-dhcp-server";
prefix dhc4-srv;
import ietf-inet-types {
prefix inet;
}
import ietf-interfaces {
prefix if;
}
import ietf-yang-types {
prefix yang;
}
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "YANG model for configuring a DHCPv4 server";
revision 2025-01-13 {
description "Second revision based on core team review.";
reference "internal";
}
revision 2024-08-18 {
description "Initial revision adapted for Infix from DHCPv6 model.";
reference "internal";
}
typedef octet-string {
description "A generic string or a hex value if prefixed with 'id:'.";
type union {
type string {
pattern 'id:([0-9a-fA-F]{2}:)*[0-9a-fA-F]{2}';
}
type string;
}
}
typedef dhcp-server-options {
description "Supported DHCP server options.";
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 configuraiton
exists in global/subnet/host scope. Except in the case when the
server respods to a relayed request, in which case the subnet will
not match any local interface.";
}
enum router {
value 3;
description "Default route(s)";
}
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 configuraiton
exists in global/subnet/host scope. Except in the case when the
server respods to a relayed request, in which case the subnet will
not match any local interface.";
}
enum ntp-server {
value 42;
description "NTP server";
}
enum domain-search {
value 119;
description "Domain search list";
}
enum classless-static-route {
value 121;
description "Classless static routes";
}
}
}
}
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-server-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 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 {
leaf client-id {
description "Match on client-id (string 'abc' or hex 'id:c0:ff:ee', DHCP option 61.";
type union {
type yang:mac-address;
type 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;
container sent {
description "The packets sent by the server.";
leaf offer-count {
type yang:counter32;
description "Total number of DHCPOFFER packets.";
}
leaf ack-count {
type yang:counter32;
description "Total number of DHCPACK packets.";
}
leaf nak-count {
type yang:counter32;
description "Total number of DHCPNAK packets.";
}
}
container received {
description "The packets sent by the client.";
leaf decline-count {
type yang:counter32;
description "Total number of DHCPDECLINE packets.";
}
leaf discover-count {
type yang:counter32;
description "Total number of DHCPDISCOVER packets.";
}
leaf request-count {
type yang:counter32;
description "Total number of DHCPREQUEST packets.";
}
leaf release-count {
type yang:counter32;
description "Total number of DHCPRELEASE packets.";
}
leaf inform-count {
type yang:counter32;
description "Total number of DHCPINFORM packets.";
}
}
action clear {
description "Clear packet statistics.";
}
}
}
}
@@ -1,329 +0,0 @@
module infix-dhcp-server {
yang-version 1.1;
namespace "urn:infix:params:xml:ns:yang:infix-dhcp-server";
prefix dhc4-srv;
import ietf-inet-types {
prefix inet;
}
import ietf-interfaces {
prefix if;
}
import ietf-ip {
prefix ip;
}
import ietf-yang-types {
prefix yang;
}
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "YANG model for configuring a DHCPv4 server";
revision 2024-08-18 {
description "Initial revision.";
reference "rfc2131 rfc7950";
}
typedef dhcp-server-options {
type union {
type string;
type enumeration {
enum router {
value 3;
description "Default router.";
}
enum dns-server {
value 6;
description "DNS server.";
}
enum domain-name {
value 15;
description "Domain name.";
}
enum mtu {
value 26;
description "Interface MTU.";
}
enum ntp-server {
value 42;
description "NTP server.";
}
enum netbios-ns {
value 44;
description "NETBIOS name server.";
}
enum netbios-nodetype {
value 46;
description "NETBIOS node type.";
}
enum netbios-scope {
value 47;
description "NETBIOS scope.";
}
}
}
description "Supported DHCP server options.";
}
container dhcp-server {
description "DHCP server configuration.";
leaf enabled {
type boolean;
default "true";
description "Globally enables the DHCP server function.";
}
list server-if {
key "if-name";
description "List of interfaces configuring a DHCP server.";
leaf if-name {
type if:interface-ref;
mandatory true;
description "Name of the interface.";
}
leaf enabled {
type boolean;
default "true";
description "Enable DHCP server for this interface.";
}
leaf dns-enabled {
type boolean;
default "true";
description "Enable DNS service.";
}
leaf mode {
description "Operating mode of the DHCP server.";
default "server";
type enumeration {
enum server {
description "DHCP server operates as server.";
}
enum relay {
description "DHCP server operates as relay.";
}
}
}
container server {
leaf address {
type inet:ip-address;
description "IP address of the server to listen on (optional).
If not set, the server will listen to all local
addresses configured on the interface.";
}
leaf lease-time {
type uint32 {
range "180..31536000";
}
units "seconds";
default 3600;
description "Default network address lease time assigned to DHCP clients.";
}
list option {
key "name";
description "List of DHCP options to offer.";
leaf name {
type dhcp-server-options;
description "DHCP option to offer.";
}
leaf value {
type string;
description "Optional value, only used for non-flag options.
Example: option:dns, value:1.2.3.4
option:vendor, value:01:02:03:04:05:06:07:08:09:0a
option:domain, value:example.com";
}
}
list ip-pool {
key "pool-name";
description "IP pool configuration.";
leaf pool-name {
type string {
length "1..64";
}
description "Name of the IP pool.";
}
leaf lease-time {
type uint32 {
range "180..31536000";
}
units "seconds";
default 3600;
description "Network address lease time assigned to DHCP clients.";
}
leaf first-ip-address {
type inet:ip-address;
mandatory "true";
description "First IP address of the pool.";
}
leaf last-ip-address {
type inet:ip-address;
mandatory "true";
description "Last IP address of the pool.";
}
list static-allocation {
key "ip-address";
description "List of host using static address allocation.";
leaf ip-address {
type inet:ip-address;
description "IP address of the host.";
}
choice identifier {
description "The identifier for the host.";
case mac-address {
leaf mac-address {
type yang:mac-address;
description "Client MAC address of the host.";
}
}
case hostname {
leaf hostname {
type yang:mac-address;
description "Client hostname (DHCP option 12) of the host.";
}
}
case client-identifier {
leaf client-identifier {
type yang:mac-address;
description "Client identifier (DHCP option 61) of the host.";
}
}
}
}
}
container lease {
description "Lease information.";
config "false";
leaf host-count {
type uint32;
description "Number of host leases.";
}
list host {
description "List of active host leases.";
key ip-address;
leaf ip-address {
type inet:ip-address;
description "IP address of the host.";
}
leaf hardware-address {
type string;
description "MAC address of the host.";
}
leaf hostname {
type string;
description "Name of the host.";
}
leaf expires {
type yang:date-and-time;
description "Time and date when the lease expires.";
}
leaf client-identifier {
type string;
description "Client identifier of the host.";
}
}
}
}
container relay {
leaf address {
type inet:ip-address;
description "IP address of the server to listen on.
If not set, the server will listen to all local
addresses configured on the interface.";
}
list server {
key "ip-address";
description "List of addresses to which the server will relay to.";
leaf ip-address {
type inet:ip-address;
description "IP address of the relay server.";
}
}
leaf circuit-id {
type yang:hex-string {
length "2..11";
}
description "Relay Agent Circuit Identifier
to be inserted as DHCP option 82 when relaying requests to the DHCP server.
If not set, the interface index will be used.";
}
leaf remote-id {
type yang:hex-string {
length "2..11";
}
description "Relay Agent Remote Identifier
to be inserted as DHCP option 82 when relaying requests to the DHCP server.";
}
}
container packet-statistics {
description "DHCP packet statistics.";
config "false";
container sent {
description "The packets sent by the server.";
leaf offer-count {
type uint32;
description "Total number of DHCPOFFER packets.";
}
leaf ack-count {
type uint32;
description "Total number of DHCPACK packets.";
}
leaf nack-count {
type uint32;
description "Total number of DHCPNAK packets.";
}
}
container received {
description "The packets sent by the client.";
leaf decline-count {
type uint32;
description "Total number of DHCPDECLINE packets.";
}
leaf discover-count {
type uint32;
description "Total number of DHCPDISCOVER packets.";
}
leaf request-count {
type uint32;
description "Total number of DHCPREQUEST packets.";
}
leaf release-count {
type uint32;
description "Total number of DHCPRELEASE packets.";
}
leaf inform-count {
type uint32;
description "Total number of DHCPINFORM packets.";
}
}
}
}
}
rpc clean-statistics {
description "Clear packet statistics.";
input {
leaf if-name {
type if:interface-ref;
mandatory true;
description "Name of the interface.";
}
}
}
}
+1
View File
@@ -0,0 +1 @@
infix-dhcp-server.yang
+16 -8
View File
@@ -224,9 +224,8 @@
</COMMAND>
<COMMAND name="dhcp-server" help="DHCP server tools" mode="switch">
<COMMAND name="clean-statistics" help="Clean DHCP server packet statistics">
<PARAM name="if-name" ptype="/IFACES" help="DHCP server interface."/>
<ACTION sym="srp_rpc@sysrepo">/infix-dhcp-server:clean-statistics</ACTION>
<COMMAND name="clear-statistics" help="Clear DHCP server statistics">
<ACTION sym="srp_rpc@sysrepo">/infix-dhcp-server:dhcp-server/statistics/clear</ACTION>
</COMMAND>
</COMMAND>
@@ -308,13 +307,22 @@
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
/usr/libexec/statd/cli-pretty "show-dhcp-server"
</ACTION>
<SWITCH name="optional" min="0">
<COMMAND name="detailed" help="Detailed (full) output">
<ACTION sym="script">
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
jq -C .
</ACTION>
<COMMAND name="statistics" help="Show DHCP server statistics">
<ACTION sym="script">
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
/usr/libexec/statd/cli-pretty "show-dhcp-server" -s
</ACTION>
</COMMAND>
<COMMAND name="detailed" help="Detailed (full operational) output">
<ACTION sym="script">
sysrepocfg -f json -X -d operational -m infix-dhcp-server | \
jq -C .
</ACTION>
</COMMAND>
</SWITCH>
</COMMAND>
+77 -28
View File
@@ -58,11 +58,11 @@ class PadSoftware:
version = 23
class PadDhcpServer:
iface = 7
ip = 17
mac = 19
host = 21
exp = 7
cid = 19
exp = 10
class PadUsbPort:
title = 30
@@ -359,32 +359,78 @@ class STPPortID:
class DhcpServer:
def __init__(self, data):
self.data = data
self.iface = get_json_data('', self.data, 'if-name')
self.leases = []
now = datetime.now(timezone.utc)
for lease in get_json_data([], self.data, 'server', 'lease', 'host'):
dt = datetime.strptime(lease["expires"], "%Y-%m-%dT%H:%M:%S%z")
exp = (dt - now).total_seconds()
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["ip-address"],
"mac": lease["hardware-address"],
"ip": lease["address"],
"mac": lease["phys-address"],
"cid": lease["client-id"],
"host": lease["hostname"],
"exp": int(exp)
"exp": exp
})
stats = get_json_data([], self.data, 'statistics')
self.offers = stats["sent"]["offer-count"]
self.acks = stats["sent"]["ack-count"]
self.naks = stats["sent"]["nak-count"]
self.declines = stats["received"]["decline-count"]
self.discovers = stats["received"]["discover-count"]
self.requests = stats["received"]["request-count"]
self.releases = stats["received"]["release-count"]
self.informs = stats["received"]["inform-count"]
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"]
exp = "%ds" % lease["exp"]
ip = lease["ip"]
mac = lease["mac"]
cid = lease["cid"]
exp = lease['exp']
host = lease["host"][:20]
row = f"{self.iface:<{PadDhcpServer.iface}}"
row += f"{ip:<{PadDhcpServer.ip}}"
row = f"{ip:<{PadDhcpServer.ip}}"
row += f"{mac:<{PadDhcpServer.mac}}"
row += f"{host:<{PadDhcpServer.host}}"
row += f"{exp:<{PadDhcpServer.exp}}"
row += f"{cid:<{PadDhcpServer.cid}}"
row += f"{exp:>{PadDhcpServer.exp - 1}}"
print(row)
def print_stats(self):
print(f"{'DHCP offers sent':<{32}}: {self.offers}")
print(f"{'DHCP ACK messages sent':<{32}}: {self.acks}")
print(f"{'DHCP NAK messages sent':<{32}}: {self.naks}")
print(f"{'DHCP decline messages received':<{32}}: {self.declines}")
print(f"{'DHCP discover messages received':<{32}}: {self.discovers}")
print(f"{'DHCP request messages received':<{32}}: {self.requests}")
print(f"{'DHCP release messages received':<{32}}: {self.discovers}")
print(f"{'DHCP inform messages received':<{32}}: {self.discovers}")
class Iface:
def __init__(self, data):
self.data = data
@@ -1072,21 +1118,23 @@ def show_ntp(json):
row += f"{source['poll']:>{PadNtpSource.poll}}"
print(row)
def show_dhcp_server(json):
if not json.get("infix-dhcp-server:dhcp-server"):
def show_dhcp_server(json, stats):
data = json.get("infix-dhcp-server:dhcp-server")
if not data:
print("DHCP server not enabled.")
return
hdr = (f"{'IFACE':<{PadDhcpServer.iface}}"
f"{'IP':<{PadDhcpServer.ip}}"
f"{'MAC':<{PadDhcpServer.mac}}"
f"{'HOSTNAME':<{PadDhcpServer.host}}"
f"{'EXPIRES':<{PadDhcpServer.exp}}")
print(Decore.invert(hdr))
server = DhcpServer(data)
servers = get_json_data({}, json, "infix-dhcp-server:dhcp-server", "server-if")
for s in servers:
server = DhcpServer(s)
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():
@@ -1125,7 +1173,8 @@ def main():
parser_show_boot_order = subparsers.add_parser('show-boot-order', help='Show NTP sources')
parser_show_routing_table = subparsers.add_parser('show-dhcp-server', help='Show DHCP server')
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
@@ -1145,7 +1194,7 @@ def main():
elif args.command == "show-ntp":
show_ntp(json_data)
elif args.command == "show-dhcp-server":
show_dhcp_server(json_data)
show_dhcp_server(json_data, args.stats)
else:
print(f"Error, unknown command '{args.command}'")
sys.exit(1)
@@ -1,82 +1,37 @@
#!/usr/bin/python3
#
# This script is used to query dnsmasq daemons via dbus and
# fills/cleans the infix-dhcp-server packet statistics.
#
"""
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
import re
DBUS_NAME = "org.freedesktop.DBus"
DBUS_OBJECT = "/org/freedesktop/DBus"
DBUS_IFACE = "org.freedesktop.DBus"
DNSMASQ_NAME = "uk.org.thekelleys.dnsmasq"
DNSMASQ_IFACE = "uk.org.thekelleys.dnsmasq"
DNSMASQ_OBJECT = "/uk/org/thekelleys/dnsmasq"
def get_servers(bus):
try:
remote_object = bus.get_object(DBUS_NAME, DBUS_OBJECT)
iface = dbus.Interface(remote_object, DBUS_IFACE)
except dbus.DBusException:
return []
servers = []
for name in iface.ListNames():
r = re.search(r"%s.(\w+)$" % DNSMASQ_NAME, name)
if r:
server = {
"ifc": r.group(1),
"name": str(name)
}
servers.append(server)
return servers
def get_iface(bus, name):
try:
remote_object = bus.get_object(name, DNSMASQ_OBJECT)
iface = dbus.Interface(remote_object, DNSMASQ_IFACE)
except dbus.DBusException:
return None
finally:
return iface
def main():
bus = dbus.SystemBus()
servers = get_servers(bus)
parser = argparse.ArgumentParser(prog='dhcp-server-status')
parser.add_argument("-c", "--clean", help="DHCP server interface")
parser.add_argument("-c", "--clear", action="store_true",
help="Clear DHCP server metrics")
args = parser.parse_args()
if args.clean:
for server in servers:
if server["ifc"] != args.clean:
continue
iface = get_iface(bus, server["name"])
if not iface:
continue
print("Cleaning metrics for DHCP server on %s" % server["ifc"])
iface.ClearMetrics()
else:
data = []
for server in servers:
iface = get_iface(bus, server["name"])
if not iface:
continue
data.append({
"if-name": server["ifc"],
"metrics": iface.GetMetrics()
})
print(json.dumps(data))
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__":
+1 -3
View File
@@ -6,8 +6,7 @@ license = "MIT"
packages = [
{ include = "yanger" },
{ include = "cli_pretty" },
{ include = "ospf_status" },
{ include = "dhcp_server_status" }
{ include = "ospf_status" }
]
authors = [
"KernelKit developers"
@@ -22,4 +21,3 @@ build-backend = "poetry.core.masonry.api"
yanger = "yanger.__main__:main"
cli-pretty = "cli_pretty:main"
ospf-status = "ospf_status:main"
dhcp-server-status = "dhcp_server_status:main"
+81 -59
View File
@@ -1,79 +1,101 @@
from datetime import datetime
from .host import HOST
#!/usr/bin/env python3
"""
Collect operational data for infix-dhcp-server.yang from dnsmasq
"""
from datetime import datetime, timezone
import json
import dbus
def lease_info(ifname):
def leases(leases_file):
"""Populate DHCP leases table"""
leases = f"/var/run/dnsmasq-{ifname}.leases"
hosts = []
table = []
try:
with open(leases, 'r', encoding='utf-8') as fd:
with open(leases_file, 'r', encoding='utf-8') as fd:
for line in fd:
tokens = line.strip().split(" ")
if len(tokens) != 5:
continue
host = {
"ip-address": tokens[2],
"hardware-address": tokens[1],
# 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],
}
dt = datetime.utcfromtimestamp(int(tokens[0]))
host["expires"] = dt.isoformat() + "+00:00"
if tokens[3] != '*':
host["hostname"] = tokens[3]
if tokens[4] != '*':
host["client-identifier"] = tokens[4]
row["hostname"] = tokens[3]
else:
row["hostname"] = ""
hosts.append(host)
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 {
"host-count": len(hosts),
"host": hosts
}
def status(servers):
"""Populate DHCP server status"""
data = HOST.run_json(['/usr/libexec/statd/dhcp-server-status'], default=[])
if data == []:
return
for entry in data:
metrics = entry["metrics"]
servers.append({
"if-name": entry["if-name"],
"packet-statistics": {
"sent": {
"offer-count": metrics["dhcp_offer"],
"ack-count": metrics["dhcp_ack"],
"nak-count": metrics["dhcp_nak"]
},
"received": {
"decline-count": metrics["dhcp_decline"],
"discover-count": metrics["dhcp_discover"],
"request-count": metrics["dhcp_request"],
"release-count": metrics["dhcp_release"],
"inform-count": metrics["dhcp_inform"]
}
},
"server": {
"lease": lease_info(entry["if-name"])
}
})
def operational():
"""Return operational status for DHCP server"""
out = {
"infix-dhcp-server:dhcp-server": {
"server-if": []
"sent": {
"offer-count": metrics["dhcp_offer"],
"ack-count": metrics["dhcp_ack"],
"nak-count": metrics["dhcp_nak"]
},
"received": {
"decline-count": metrics["dhcp_decline"],
"discover-count": metrics["dhcp_discover"],
"request-count": metrics["dhcp_request"],
"release-count": metrics["dhcp_release"],
"inform-count": metrics["dhcp_inform"]
}
}
status(out['infix-dhcp-server:dhcp-server']['server-if'])
return out
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")))