From 3c1e9f3199572fd181a190a1e571f6f55daf44bf Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 13 Jan 2025 19:23:22 +0100 Subject: [PATCH] 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 --- board/common/rootfs/etc/dnsmasq.conf | 15 +- package/python-statd/python-statd.mk | 1 - .../etc/finit.d/available/dnsmasq.conf | 2 +- src/confd/src/infix-dhcp-server.c | 719 ++++++++---------- src/confd/yang/confd.inc | 2 +- src/confd/yang/infix-dhcp-server.yang | 409 ++++++++++ .../yang/infix-dhcp-server@2024-08-18.yang | 329 -------- .../yang/infix-dhcp-server@2025-01-13.yang | 1 + src/klish-plugin-infix/xml/infix.xml | 24 +- src/statd/python/cli_pretty/cli_pretty.py | 105 ++- .../dhcp_server_status/dhcp_server_status.py | 83 +- src/statd/python/pyproject.toml | 4 +- src/statd/python/yanger/infix_dhcp_server.py | 140 ++-- 13 files changed, 943 insertions(+), 891 deletions(-) create mode 100644 src/confd/yang/infix-dhcp-server.yang delete mode 100644 src/confd/yang/infix-dhcp-server@2024-08-18.yang create mode 120000 src/confd/yang/infix-dhcp-server@2025-01-13.yang mode change 100644 => 100755 src/statd/python/yanger/infix_dhcp_server.py diff --git a/board/common/rootfs/etc/dnsmasq.conf b/board/common/rootfs/etc/dnsmasq.conf index 15b085a6..557cc1ac 100644 --- a/board/common/rootfs/etc/dnsmasq.conf +++ b/board/common/rootfs/etc/dnsmasq.conf @@ -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 - diff --git a/package/python-statd/python-statd.mk b/package/python-statd/python-statd.mk index f8508244..e2d0d350 100644 --- a/package/python-statd/python-statd.mk +++ b/package/python-statd/python-statd.mk @@ -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)) diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/dnsmasq.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/dnsmasq.conf index 1d7900af..a14723e8 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/dnsmasq.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/dnsmasq.conf @@ -1 +1 @@ -service [S12345] dnsmasq -k -u root -- DHCP/DNS proxy +service [S12345] dnsmasq -k -u root -- DHCP/DNS proxy diff --git a/src/confd/src/infix-dhcp-server.c b/src/confd/src/infix-dhcp-server.c index 7afa0d57..ad46ba3d 100644 --- a/src/confd/src/infix-dhcp-server.c +++ b/src/confd/src/infix-dhcp-server.c @@ -10,28 +10,31 @@ #include "core.h" #include -#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 \\\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, "\n"); - fprintf(fp, "\n"); - fprintf(fp, " \n"); - fprintf(fp, " \n", DBUS_NAME_DNSMASQ, ifname); - fprintf(fp, " \n", DBUS_NAME_DNSMASQ, ifname); - fprintf(fp, " \n"); - fprintf(fp, " \n"); - fprintf(fp, " \n", DBUS_NAME_DNSMASQ, ifname); - fprintf(fp, " \n", DBUS_NAME_DNSMASQ, ifname); - fprintf(fp, " \n"); - fprintf(fp, "\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: diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 82cdb925..02e8c4f6 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -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" diff --git a/src/confd/yang/infix-dhcp-server.yang b/src/confd/yang/infix-dhcp-server.yang new file mode 100644 index 00000000..ce3ad931 --- /dev/null +++ b/src/confd/yang/infix-dhcp-server.yang @@ -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."; + } + } + } +} diff --git a/src/confd/yang/infix-dhcp-server@2024-08-18.yang b/src/confd/yang/infix-dhcp-server@2024-08-18.yang deleted file mode 100644 index a0071b4b..00000000 --- a/src/confd/yang/infix-dhcp-server@2024-08-18.yang +++ /dev/null @@ -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."; - } - } - } -} diff --git a/src/confd/yang/infix-dhcp-server@2025-01-13.yang b/src/confd/yang/infix-dhcp-server@2025-01-13.yang new file mode 120000 index 00000000..77a3d375 --- /dev/null +++ b/src/confd/yang/infix-dhcp-server@2025-01-13.yang @@ -0,0 +1 @@ +infix-dhcp-server.yang \ No newline at end of file diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml index caf75792..451b5c85 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -224,9 +224,8 @@ - - - /infix-dhcp-server:clean-statistics + + /infix-dhcp-server:dhcp-server/statistics/clear @@ -308,13 +307,22 @@ sysrepocfg -f json -X -d operational -m infix-dhcp-server | \ /usr/libexec/statd/cli-pretty "show-dhcp-server" + - - - sysrepocfg -f json -X -d operational -m infix-dhcp-server | \ - jq -C . - + + + 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 | \ + jq -C . + + + diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index 7fca71d0..f5541cf6 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -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) diff --git a/src/statd/python/dhcp_server_status/dhcp_server_status.py b/src/statd/python/dhcp_server_status/dhcp_server_status.py index 2c61353e..762c5b2d 100755 --- a/src/statd/python/dhcp_server_status/dhcp_server_status.py +++ b/src/statd/python/dhcp_server_status/dhcp_server_status.py @@ -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__": diff --git a/src/statd/python/pyproject.toml b/src/statd/python/pyproject.toml index dc4e0e45..8f33838a 100644 --- a/src/statd/python/pyproject.toml +++ b/src/statd/python/pyproject.toml @@ -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" diff --git a/src/statd/python/yanger/infix_dhcp_server.py b/src/statd/python/yanger/infix_dhcp_server.py old mode 100644 new mode 100755 index 41c22f0b..04bb5b3c --- a/src/statd/python/yanger/infix_dhcp_server.py +++ b/src/statd/python/yanger/infix_dhcp_server.py @@ -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")))