h/m/customs: migration from isc-dhcp4 to kea

Kea configuration added in its own file for general cleanness.
Migrated only lan and bms subnets; others appear to be legacy leftovers.
IP reservations migrated as-is; "one-liner" for that in comments.
Hopefully legacy "bootp" is not actually needed as that's behind a
paywall.
Generated config tested using `kea-dhcp4 -tT -c ./generated/dhcp4.conf`
Drive-by fix for checkinator to keep it working with old config with no
DHCP_SERVER config key.
Added myself to OWNERS as I'm making frequent changes here recently, and
vuko is absent.

Change-Id: I5d5dd71ab4fd3fb498bd8bc95428984b3b08f092
Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1943
Reviewed-by: q3k <q3k@hackerspace.pl>
Reviewed-by: informatic <informatic@hackerspace.pl>
diff --git a/hswaw/checkinator/at/tracker.py b/hswaw/checkinator/at/tracker.py
index 4ec9e70..5c0656c 100644
--- a/hswaw/checkinator/at/tracker.py
+++ b/hswaw/checkinator/at/tracker.py
@@ -78,10 +78,13 @@
     args = parser.parse_args()
 
     config = yaml.safe_load(args.config.read_text())
-    if config['DHCP_SERVER'] == "kea":
-        tracker = KeaUpdater(config['KEA_LEASE_FILE'], config['TIMEOUT'])
-    elif config['DHCP_SERVER'] == "isc" or "DHCP_SERVER" not in config:
+    server = config.get('DHCP_SERVER', 'isc')
+    if server == 'isc':
         tracker = DhcpdUpdater(config['LEASE_FILE'], config['TIMEOUT'])
+    elif server == 'kea':
+        tracker = KeaUpdater(config['KEA_LEASE_FILE'], config['TIMEOUT'])
+    else:
+        raise Exception('invalid DHCP_SERVER value, expected isc or kea')
     tracker.start()
 
     server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
diff --git a/hswaw/machines/OWNERS b/hswaw/machines/OWNERS
index c1ce290..0802de4 100644
--- a/hswaw/machines/OWNERS
+++ b/hswaw/machines/OWNERS
@@ -2,3 +2,4 @@
 owners:
   - informatic
   - vuko
+  - ar
diff --git a/hswaw/machines/customs.hackerspace.pl/checkinator-tracker.nix b/hswaw/machines/customs.hackerspace.pl/checkinator-tracker.nix
index 47a5a71..b1ae278 100644
--- a/hswaw/machines/customs.hackerspace.pl/checkinator-tracker.nix
+++ b/hswaw/machines/customs.hackerspace.pl/checkinator-tracker.nix
@@ -25,6 +25,8 @@
   config = builtins.toFile "${name}-config.yaml" (pkgs.lib.generators.toYAML {} {
     # path to dhcpd lease file
     LEASE_FILE = "/var/lib/dhcpd4/dhcpd.leases";
+    KEA_LEASE_FILE = "/var/lib/kea/dhcp4.leases";
+    DHCP_SERVER = "kea";
 
     # timeout for old leases
     TIMEOUT = 1500;
diff --git a/hswaw/machines/customs.hackerspace.pl/configuration.nix b/hswaw/machines/customs.hackerspace.pl/configuration.nix
index a52d92e..f985313 100644
--- a/hswaw/machines/customs.hackerspace.pl/configuration.nix
+++ b/hswaw/machines/customs.hackerspace.pl/configuration.nix
@@ -56,6 +56,7 @@
       ./doorman/service.nix
       ./beyondspace.nix
       ./laserproxy/service.nix
+      ./kea.nix
     ];
 
   # Prevent spurious rebuilds due to dbus override on minimal profile
@@ -332,12 +333,6 @@
 
   users.users.root.openssh.authorizedKeys.keys = [ vuko-pubkey q3k-pubkey ];
 
-  services.dhcpd4 = {
-    enable = true;
-    configFile = "${./dhcpd.conf}";
-    interfaces = ["lan" "bms"];
-  };
-
   services.mosquitto.enable = true;
   services.mosquitto.listeners = [
     {
@@ -352,16 +347,6 @@
     }
   ];
 
-  # Checkinator needs access to leases file. When DynamicUser is enable this
-  # file is hidden in /var/lib/private
-  systemd.services.dhcpd4.serviceConfig.DynamicUser= pkgs.lib.mkForce false;
-  users.users.dhcpd = {
-    group = "dhcpd";
-    isSystemUser = true;
-    uid = 1005;
-  };
-  users.groups."dhcpd" = {};
-
   hscloud.routing = {
     enable = true;
     # TODO(q3k): make this optional in upstream
diff --git a/hswaw/machines/customs.hackerspace.pl/dhcpd.conf b/hswaw/machines/customs.hackerspace.pl/dhcpd.conf
deleted file mode 100644
index 1b47699..0000000
--- a/hswaw/machines/customs.hackerspace.pl/dhcpd.conf
+++ /dev/null
@@ -1,207 +0,0 @@
-option domain-search "waw.hackerspace.pl";
-option domain-name-servers 10.8.1.2;
-default-lease-time 600;
-max-lease-time 600;
-one-lease-per-client true;
-
-option cisco-ip-phone-tftp code 150 = ip-address;
-
-subnet 10.8.0.0 netmask 255.255.0.0 {
-
-    option routers 10.8.1.2;
-    range 10.8.0.20 10.8.0.199;
-    authoritative;
-    allow bootp;
-
-    if substring (option vendor-class-identifier, 15, 5) = "00000" {
-        filename "netboot.xyz.kpxe";
-    } else {
-        filename "netboot.xyz.efi";
-    }
-
-    next-server 10.8.1.2;
-
-    host laser {
-        hardware ethernet 00:0e:35:1d:a1:a4;
-        fixed-address 10.8.1.18;
-    }
-    host oki {
-        hardware ethernet 00:25:36:de:27:56;
-        fixed-address 10.8.1.20;
-    }
-    host brother {
-        hardware ethernet 00:1b:a9:24:96:e2;
-        fixed-address 10.8.1.21;
-    }
-    host zebra {
-        hardware ethernet 00:07:4d:4d:71:e4;
-        fixed-address 10.8.1.22;
-    }
-    host lj2100 {
-        hardware ethernet 00:30:C1:62:61:23;
-        fixed-address 10.8.1.23;
-    }
-    host dht21 {
-        hardware ethernet 5c:cf:7f:06:9a:3e;
-        fixed-address 10.8.1.25;
-    }
-    host ledpanel {
-        hardware ethernet 00:0A:35:00:01:22;
-        fixed-address 10.8.1.26;
-    }
-    host printmaster {
-        hardware ethernet b8:27:eb:ed:df:f9;
-        fixed-address 10.8.1.17;
-    }
-    host bridgeport {
-        hardware ethernet 90:1b:0e:1d:23:09;
-        fixed-address 10.8.1.29;
-    }
-    host 3printers1cups {
-        hardware ethernet 02:20:f5:20:6a:2d;
-        fixed-address 10.8.1.30;
-    }
-
-    host telelele {
-        hardware ethernet fe:77:d6:83:26:b1;
-        fixed-address 10.8.1.31;
-    }
-
-    # vending
-    host vending {
-        #hardware ethernet b8:27:eb:71:e4:0e;
-	#hardware ethernet b8:27:eb:3d:ba:fe;
-        hardware ethernet b8:27:eb:03:69:01;
-        fixed-address 10.8.1.32;
-    }
-
-    host transcend {
-        hardware ethernet b0:38:29:2e:5d:c9;
-        fixed-address 10.8.1.33;
-    }
-
-    host welcomer {
-        hardware ethernet b8:27:eb:37:9e:6e;
-        fixed-address 10.8.1.34;
-    }
-    
-    host arcade {
-        hardware ethernet 00:23:ae:6f:8e:a7;
-        fixed-address 10.8.1.35;
-    }
-
-    host inventory {
-        hardware ethernet 90:e6:ba:84:b6:e0;
-        fixed-address 10.8.1.38;
-    }
-
-    host camera {
-        hardware ethernet 52:54:00:1f:63:1b;
-        fixed-address 10.8.1.39;
-    }
-
-    # Cisco IP Phone
-    host SEPA40CC394DB0C {
-        hardware ethernet a4:0c:c3:94:db:0c;
-        next-server 10.8.1.2;
-	# managed by dfgg/drozdziak
-        #option cisco-ip-phone-tftp 10.8.0.190;
-        option cisco-ip-phone-tftp 10.8.1.2;
-        fixed-address 10.8.1.42;
-    }
-
-    # RIPE Atlas Probe
-    host ripeatlas {
-        hardware ethernet c0:25:e9:99:fb:e8;
-        fixed-address 10.8.1.43;
-    }
-    
-    host chromecast {
-        hardware ethernet 6c:ad:f8:52:4c:a7;
-        fixed-address 10.8.1.47;
-    }
-
-    # craptrap VM
-    host winbox {
-        hardware ethernet 52:54:00:D9:DB:42;
-        fixed-address 10.8.1.48;
-    }
-    
-    host staszkecoin {
-        hardware ethernet 02:42:24:75:eb:19;
-        fixed-address 10.8.1.49;
-    }
-
-
-    host blitzloop {
-        hardware ethernet 00:23:14:b0:ec:c8;
-        fixed-address 10.8.1.51;
-    }
-
-    host tronxy {
-        hardware ethernet 00:1f:16:1c:47:df;
-        fixed-address 10.8.1.52;
-    }
-
-    host tv1 {
-        hardware ethernet dc:a6:32:b1:68:d7;
-        fixed-address 10.8.1.53;
-    }
-    host tv2 {
-        hardware ethernet dc:a6:32:b1:68:83;
-        fixed-address 10.8.1.54;
-    }
-
-    # kodak
-    host akamanto {
-        hardware ethernet b8:27:eb:d7:b9:ae;
-        fixed-address 10.8.1.55;
-    }
-
-    # voron
-    host karasu-tengu {
-        hardware ethernet b8:27:eb:00:f8:f5;
-        fixed-address 10.8.1.56;
-    }
-}
-
-# Printer subnet (10.10.7.0/24) has ip-helper 10.8.1.2 set on hs-core01.
-# Make DHCP happen.
-subnet 10.10.7.0 netmask 255.255.255.0 {
-    option routers 10.10.7.1;
-    range 10.10.7.100 10.10.7.200;
-    authoritative;
-    allow bootp;
-}
-
-# Listen for relayed requests on the interface from core01
-# (even though we're not serving anything there directly)
-subnet 172.16.1.0 netmask 255.255.255.250 {
-}
-
-subnet 10.10.5.0 netmask 255.255.255.0 {
-    option routers 10.10.5.1;
-    range 10.10.5.100 10.10.5.200;
-    authoritative;
-    allow bootp;
-    filename "elilo.efi";
-    next-server 10.8.1.16;
-#    option pxelinux.configfile "elilo.conf"
-}
-# itanic ilo gnuj
-subnet 10.10.1.0 netmask 255.255.255.0 {
-    option routers 10.10.1.1;
-    range 10.10.1.150 10.10.1.200;
-    authoritative;
-    next-server 10.8.1.16;
-}
-
-
-subnet 10.11.1.0 netmask 255.255.255.0 {
-    option routers 10.11.1.1;
-    option domain-name-servers 10.11.1.1;
-    range 10.11.1.100 10.11.1.200;
-    authoritative;
-    default-lease-time 600;
-    max-lease-time 600;
-}
diff --git a/hswaw/machines/customs.hackerspace.pl/kea.nix b/hswaw/machines/customs.hackerspace.pl/kea.nix
new file mode 100644
index 0000000..8ef9933
--- /dev/null
+++ b/hswaw/machines/customs.hackerspace.pl/kea.nix
@@ -0,0 +1,136 @@
+{ config, pkgs, lib, ... }:
+let
+  keaReservationsPreformat = lib.attrsets.mapAttrsToList (name: val: {
+    hostname = val.hostname;
+    hw-address = name;
+    ip-address = val.ip;
+  });
+in {
+  services.kea = {
+    dhcp4 = {
+      enable = true;
+      settings = {
+        interfaces-config = { interfaces = [ "lan" "bms" ]; };
+
+        lease-database = {
+          name = "/var/lib/kea/dhcp4.leases";
+          persist = true;
+          type = "memfile";
+        };
+
+        rebind-timer = 300;
+        renew-timer = 150;
+        valid-lifetime = 600;
+
+        # yanked from https://kea.readthedocs.io/en/latest/arm/dhcp4-srv.html#setting-fixed-fields-in-classification
+        # if i understand correctly, the logic is reversed to what isc-dhcp
+        # config did, but result should be the same
+        client-classes = [{
+          name = "ipxe_efi_x64";
+          test = "option[93].hex == 0x0009";
+          boot-file-name = "netboot.xyz.efi";
+        }];
+
+        subnet4 = [
+          { # general members area lan
+            subnet = "10.8.0.0/16";
+            pools = [{ pool = "10.8.0.20 - 10.8.0.199"; }];
+            reservations-out-of-pool = false;
+            reservations-in-subnet = true;
+            authoritative = true;
+
+            next-server = "10.8.1.2";
+
+            option-data = [
+              {
+                name = "routers";
+                data = "10.8.1.2";
+              }
+              {
+                name = "domain-name-servers";
+                data = "10.8.1.2";
+              }
+              {
+                name = "boot-file-name";
+                data = "netboot.xyz.kpxe";
+              }
+            ];
+
+            reservations = keaReservationsPreformat {
+              # cat old-dhcpd.conf | sed -e 's/;//g' | awk '
+              #   $1 == "host" { hostname = $2; pp = "yes"; }
+              #   $1 == "hardware" { hwaddr = $3; }
+              #   $1 == "fixed-address" { ip = $2; }
+              #   $1 == "}" && pp == "yes" {
+              #       print "\"" hwaddr "\" = { ip = \"" ip "\"; hostname = \"" hostname "\"; };"
+              #       pp = "no"
+              #   }'
+              "00:0e:35:1d:a1:a4" = { ip = "10.8.1.18"; hostname = "laser"; };
+              # set here just for completeness sake; the printer for some
+              # reason doesn't accept dhcp offers from kea.
+              "00:25:36:de:27:56" = { ip = "10.8.1.20"; hostname = "oki"; };
+              "00:1b:a9:24:96:e2" = { ip = "10.8.1.21"; hostname = "brother"; };
+              "00:07:4d:4d:71:e4" = { ip = "10.8.1.22"; hostname = "zebra"; };
+              "00:30:C1:62:61:23" = { ip = "10.8.1.23"; hostname = "lj2100"; };
+              "5c:cf:7f:06:9a:3e" = { ip = "10.8.1.25"; hostname = "dht21"; };
+              "00:0A:35:00:01:22" = { ip = "10.8.1.26"; hostname = "ledpanel"; };
+              "b8:27:eb:ed:df:f9" = { ip = "10.8.1.17"; hostname = "printmaster"; };
+              "90:1b:0e:1d:23:09" = { ip = "10.8.1.29"; hostname = "bridgeport"; };
+              "02:20:f5:20:6a:2d" = { ip = "10.8.1.30"; hostname = "3printers1cups"; };
+              "fe:77:d6:83:26:b1" = { ip = "10.8.1.31"; hostname = "telelele"; };
+              "b8:27:eb:03:69:01" = { ip = "10.8.1.32"; hostname = "vending"; };
+              "b0:38:29:2e:5d:c9" = { ip = "10.8.1.33"; hostname = "transcend"; };
+              "b8:27:eb:37:9e:6e" = { ip = "10.8.1.34"; hostname = "welcomer"; };
+              "00:23:ae:6f:8e:a7" = { ip = "10.8.1.35"; hostname = "arcade"; };
+              "90:e6:ba:84:b6:e0" = { ip = "10.8.1.38"; hostname = "inventory"; };
+              "52:54:00:1f:63:1b" = { ip = "10.8.1.39"; hostname = "camera"; };
+              # RIPE Atlas Probe
+              "c0:25:e9:99:fb:e8" = { ip = "10.8.1.43"; hostname = "ripeatlas"; };
+              "6c:ad:f8:52:4c:a7" = { ip = "10.8.1.47"; hostname = "chromecast"; };
+              # craptrap VM
+              "52:54:00:D9:DB:42" = { ip = "10.8.1.48"; hostname = "winbox"; };
+              "02:42:24:75:eb:19" = { ip = "10.8.1.49"; hostname = "staszkecoin"; };
+              "00:23:14:b0:ec:c8" = { ip = "10.8.1.51"; hostname = "blitzloop"; };
+              "00:1f:16:1c:47:df" = { ip = "10.8.1.52"; hostname = "tronxy"; };
+              "dc:a6:32:b1:68:d7" = { ip = "10.8.1.53"; hostname = "tv1"; };
+              "dc:a6:32:b1:68:83" = { ip = "10.8.1.54"; hostname = "tv2"; };
+              # kodak
+              "b8:27:eb:d7:b9:ae" = { ip = "10.8.1.55"; hostname = "akamanto"; };
+              # voron
+              "b8:27:eb:00:f8:f5" = { ip = "10.8.1.56"; hostname = "karasu-tengu"; };
+            };
+          }
+          { # bms
+            subnet = "10.11.1.0/24";
+            pools = [{ pool = "10.11.1.100 - 10.11.1.200"; }];
+            reservations-out-of-pool = false;
+            reservations-in-subnet = true;
+            authoritative = true;
+
+            option-data = [
+              {
+                name = "routers";
+                data = "10.11.1.1";
+              }
+              {
+                name = "domain-name-servers";
+                data = "10.11.1.1";
+              }
+            ];
+          }
+        ];
+      };
+    };
+  };
+
+  users.users.kea = {
+    group = "kea";
+    isSystemUser = true;
+  };
+  users.groups.kea = {};
+
+  systemd.services.kea-dhcp4-server.serviceConfig = {
+    UMask = lib.mkForce "0033";
+    DynamicUser = lib.mkForce false;
+  };
+}