mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-31 13:03:02 +02:00
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:
@@ -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__":
|
||||
|
||||
@@ -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"
|
||||
|
||||
Regular → Executable
+81
-59
@@ -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")))
|
||||
|
||||
Reference in New Issue
Block a user