changeset 326:5b88b38f2471

huge reorg, reog toplevel functions in preparation of a ui with nice task lists
author drewp@bigasterisk.com
date Mon, 20 Jan 2025 21:55:08 -0800
parents 4d1b6a6e65d2
children e796f5776e57
files .vscode/settings.json all_operations.py apt/apt.py apt/get_keyrings.sh apt/keyrings/deb.nodesource.com_gpgkey_nodesource-repo.gpg.gpg apt/keyrings/dl.google.com_linux_linux_signing_key.gpg apt/keyrings/ftp-master.debian.org_keys_archive-key-11-security.gpg apt/keyrings/ftp-master.debian.org_keys_archive-key-11.gpg apt/keyrings/hub.unity3d.com_linux_keys_public.gpg apt/keyrings/nvidia.github.io_libnvidia-container_gpgkey.gpg apt/keyrings/packages.cloud.google.com_apt_doc_apt-key.gpg apt/keyrings/packages.microsoft.com_keys_microsoft.gpg apt/keyrings/repo.steampowered.com_steam_archive_stable_steam.gpg apt/templates/more.sources.j2 apt/templates/odroid.sources.j2 apt/templates/pi.sources.j2 apt/templates/sources.list.j2 coredns_freshen.sh dns.py dns/dns.py dns/files/dnsmasq-mtail.service dns/files/metrics.mtail dns/files/resolv.conf dns/files/run_mtail.sh dns/templates/dhcp_graph_update.service.j2 dns/templates/dhcp_graph_watch.path.j2 dns/templates/dnsmasq/dnsmasq.conf.j2 dns/templates/dnsmasq/dnsmasq.service.j2 dns/templates/dnsmasq/hosts.j2 dns/templates/hosts.j2 dns/templates/resolved.conf.j2 dnsmasq_exporter.service files/dnsmasq/dnsmasq-mtail.service files/dnsmasq/metrics.mtail files/dnsmasq/run_mtail.sh files/fstab/bang files/fstab/dash files/fstab/ditto files/fstab/dot files/fstab/slash files/fstab/tofu files/kube/k3s-killall.sh files/kube/k3s-uninstall.sh files/kube/kubelet.config files/net/ditto-netplan.yaml files/net/house_net.service files/net/pipe_10.2.network files/net/pipe_isp.network files/net/prime.network files/net/singlenic.network files/pi_wlan0_powersave files/pigpiod.service home.py home/home.py k8s_reserve/deploy.yaml k8s_reserve/readme kube.py kube/coredns/coredns.yaml kube/coredns/coredns_freshen.sh kube/files/kubelet.config kube/k3s-killall.sh kube/k3s-uninstall.sh kube/k8s_reserve/deploy.yaml kube/k8s_reserve/readme kube/kube.py kube/templates/config-agent.yaml.j2 kube/templates/config-server.yaml.j2 kube/templates/k3s.service.j2 kube/templates/podman_registries.conf.j2 kube/templates/registries.yaml.j2 mail.py mail/dkim/opendkim-KeyTable mail/dkim/opendkim-SigningTable mail/dkim/opendkim-TrustedHosts mail/dkim/opendkim.conf mail/file-count/file-count.service mail/file-count/file_count.py mail/mail.py mail/main.cf.j2 mail/mydestination mail/opendkim.service multikube.py net.py net/files/ditto-netplan.yaml net/files/house_net.service net/files/pipe_10.2.network net/files/pipe_isp.network net/files/prime.network net/net.py package_lists.py packages.py packages/package_lists.py packages/packages.py pdm.lock pyproject.toml ssh.py sync.py sync/sync.py system.py system/files/ditto_exports system/files/pigpiod.service system/fstabs/bang system/fstabs/dash system/fstabs/ditto system/fstabs/dot system/fstabs/slash system/fstabs/tofu system/system.py system/templates/webforward.service.j2 tasks.py templates/bang_exports.j2 templates/dnsmasq/dnsmasq.conf.j2 templates/dnsmasq/dnsmasq.service.j2 templates/dnsmasq/hosts.j2 templates/file-count/file-count.service.j2 templates/file-count/file_count.py templates/hosts.j2 templates/kube/config-agent.yaml.j2 templates/kube/config-server.yaml.j2 templates/kube/coredns.yaml templates/kube/k3s.service.j2 templates/kube/podman_registries.conf.j2 templates/kube/registries.yaml.j2 templates/mail/main.cf.j2 templates/mail/mydestination.j2 templates/mail/opendkim-KeyTable.j2 templates/mail/opendkim-SigningTable.j2 templates/mail/opendkim-TrustedHosts.j2 templates/mail/opendkim.conf.j2 templates/mail/opendkim.service.j2 templates/resolved.conf.j2 templates/webforward.service.j2 templates/wireguard/bogasterisk.conf.j2 templates/wireguard/wg.service.j2 templates/wireguard/wg0.conf.j2 users.py users/users.py wireguard.py wireguard/templates/bogasterisk.conf.j2 wireguard/templates/wg.service.j2 wireguard/templates/wg0.conf.j2 wireguard/wireguard.py wireguard/wireguard_pubkey.py wireguard_pubkey.py
diffstat 144 files changed, 3936 insertions(+), 3766 deletions(-) [+]
line wrap: on
line diff
--- a/.vscode/settings.json	Mon Jan 20 14:10:19 2025 -0800
+++ b/.vscode/settings.json	Mon Jan 20 21:55:08 2025 -0800
@@ -1,12 +1,3 @@
 {
-  "python.linting.pylintEnabled": false,
-  "python.linting.flake8Enabled": true,
-  "python.linting.enabled": true,
-  "python.analysis.extraPaths": ["${workspaceFolder}/__pypackages__/3.11/lib"],
-  "python.autoComplete.extraPaths": ["${workspaceFolder}/__pypackages__/3.11/lib"],
-  "python.formatting.provider": "yapf",
-  "files.watcherExclude": {
-    "_darcs_old/**": true
-  },
-  "python.linting.mypyEnabled": false
+
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/all_operations.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,32 @@
+import importlib
+import os
+from pyinfra.context import host
+
+
+def gatherFuncs():
+    for opGroup in [
+            'users',
+            'system',
+            'apt',
+            'packages',
+            'net',
+            'dns',
+            'wireguard',
+            'kube',
+            'sync',
+            'mail',
+            'home',
+    ]:
+        if os.environ.get('GROUP') and opGroup != os.environ['GROUP']:
+            continue
+        mod = importlib.import_module(f'{opGroup}.{opGroup}')
+        operations = getattr(mod, 'operations')
+        for func in operations:
+            funcFullName = f'{mod.__name__}.{func.__name__}.{host.name}'
+            yield (funcFullName, func)
+
+
+funcs = list(gatherFuncs())
+
+for name, func in funcs:
+    func()
--- a/apt/apt.py	Mon Jan 20 14:10:19 2025 -0800
+++ b/apt/apt.py	Mon Jan 20 21:55:08 2025 -0800
@@ -1,32 +1,19 @@
-import shlex
+import io
 
-from pyinfra import host
+from pyinfra.context import host
 from pyinfra.facts.server import Arch
 from pyinfra.operations import apt, files, server
 
 TZ = 'America/Los_Angeles'
 
 
-def pkg_keys():
-    files.directory(path='/etc/apt/keyrings/')  # for raspi
-    for url, name in [
-        ('https://repo.steampowered.com/steam/archive/stable/steam.gpg', 'steam.gpg'),
-    ]:
-        files.download(src=url, dest=f'/usr/share/keyrings/{name}')
+def ubuntuReleases():
+    if 'pi' not in host.groups:
+        files.line(path='/etc/update-manager/release-upgrades', line="^Prompt=", replace="Prompt=normal")
 
-    apt.packages(packages=['curl', 'gpg'])
-    server.shell(commands=[
-        f"curl -fsSL {shlex.quote(url)} | gpg --dearmor > /etc/apt/keyrings/{name}" for (url, name) in [
-            ('https://packages.microsoft.com/keys/microsoft.asc', 'ms.gpg'),
-            ('https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key', 'nodesource.gpg'),
-            ('https://dl.google.com/linux/linux_signing_key.pub', 'chrome.gpg'),
-            ('https://ftp-master.debian.org/keys/archive-key-11.asc', 'bullseye.gpg'),
-            ('https://ftp-master.debian.org/keys/archive-key-11-security.asc', 'bullseye-security.gpg'),
-            ('https://packages.cloud.google.com/apt/doc/apt-key.gpg', 'coral.gpg'),
-            ('https://hub.unity3d.com/linux/keys/public', 'unityhub.gpg'),
-            ('https://nvidia.github.io/libnvidia-container/gpgkey', 'nvidia.gpg'),
-        ]
-    ])
+def pkgKeys():
+    files.directory(path='/etc/apt/keyrings/')  # for raspi
+    files.sync(src='apt/keyrings/', dest='/usr/share/keyrings/', delete=False)
 
     # also these
     #-rw-r--r-- 1 root root 2794 Mar 26  2021 /etc/apt/trusted.gpg.d/ubuntu-keyring-2012-cdimage.gpg
@@ -36,15 +23,12 @@
 
 
 def arch386():
+    if host.get_fact(Arch) != 'x86_64':
+        return
     server.shell(commands=['dpkg --add-architecture i386'])
 
 
-def old_deleteme_apt_sources():
-    files.template(src='apt/templates/sources.list.j2', dest='/etc/apt/sources.list')
-    apt_update()
-
-
-def apt_update():
+def aptUpdate():
     apt.packages(update=True,
                  cache_time=86400,
                  packages=['tzdata'],
@@ -59,22 +43,28 @@
     # and steam-launcher
 
 
-def flatpak_sources():
+def flatpakSources():
     apt.packages(update=True, cache_time=86400, packages=['flatpak'])
     server.shell(commands='flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo')
 
 
-if host.get_fact(Arch) == 'x86_64':
-    arch386()
+def sources():
+    files.put(src=io.StringIO("# see .d\n"), dest="/etc/apt/sources.list")
+    if 'pi' in host.groups:
+        osName = "pi"
+    elif host.name == 'pipe':
+        osName = "odroid"
+    else:
+        osName = "ubuntu"
+    files.template(src=f'apt/templates/{osName}.sources.j2', dest=f'/etc/apt/sources.list.d/{osName}.sources')
+    files.template(src='apt/templates/more.sources.j2', dest='/etc/apt/sources.list.d/more.sources')
 
-pkg_keys()
-using_new_sources = ['tofu']
-if host.name in using_new_sources:
-    # todo: rm /etc/apt/sources.list.d/*.list
-    files.template(src='apt/templates/ubuntu.sources.j2', dest='/etc/apt/sources.list.d/ubuntu.sources')
-    files.template(src='apt/templates/more.sources.j2', dest='/etc/apt/sources.list.d/more.sources')
-    apt_update()
-else:
-    old_deleteme_apt_sources()
 
-flatpak_sources()
+operations = [
+    ubuntuReleases,
+    arch386,
+    pkgKeys,
+    sources,
+    aptUpdate,
+    flatpakSources,
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apt/get_keyrings.sh	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,21 @@
+#!/bin/zsh
+
+
+cd `dirname $0`
+rm -f keyrings/*
+
+for url in \
+    https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
+    https://dl.google.com/linux/linux_signing_key.pub \
+    https://ftp-master.debian.org/keys/archive-key-11-security.asc \
+    https://ftp-master.debian.org/keys/archive-key-11.asc \
+    https://hub.unity3d.com/linux/keys/public \
+    https://nvidia.github.io/libnvidia-container/gpgkey
+    https://packages.cloud.google.com/apt/doc/apt-key.gpg \
+    https://packages.microsoft.com/keys/microsoft.asc \
+    https://repo.steampowered.com/steam/archive/stable/steam.gpg \
+do
+    name=`echo ${url:r}.gpg | perl -lpe 's/https:..//; s/\//_/g'`
+    curl -fsSL $url | gpg --dearmor > keyrings/$name
+done
+
Binary file apt/keyrings/deb.nodesource.com_gpgkey_nodesource-repo.gpg.gpg has changed
Binary file apt/keyrings/dl.google.com_linux_linux_signing_key.gpg has changed
Binary file apt/keyrings/ftp-master.debian.org_keys_archive-key-11-security.gpg has changed
Binary file apt/keyrings/ftp-master.debian.org_keys_archive-key-11.gpg has changed
Binary file apt/keyrings/hub.unity3d.com_linux_keys_public.gpg has changed
Binary file apt/keyrings/nvidia.github.io_libnvidia-container_gpgkey.gpg has changed
Binary file apt/keyrings/packages.cloud.google.com_apt_doc_apt-key.gpg has changed
Binary file apt/keyrings/packages.microsoft.com_keys_microsoft.gpg has changed
Binary file apt/keyrings/repo.steampowered.com_steam_archive_stable_steam.gpg has changed
--- a/apt/templates/more.sources.j2	Mon Jan 20 14:10:19 2025 -0800
+++ b/apt/templates/more.sources.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -6,79 +6,59 @@
 Suites: stable
 Components: main
 Architectures: amd64
-Signed-By: /etc/apt/keyrings/ms.gpg
+Signed-By: /usr/share/keyrings/packages.microsoft.com_keys_microsoft.gpg
+
 
 Types: deb
 URIs: http://dl.google.com/linux/chrome/deb/
 Suites: stable
 Components: main
 Architectures: amd64
-Signed-By: /etc/apt/keyrings/chrome.gpg
+Signed-By: /usr/share/keyrings/dl.google.com_linux_linux_signing_key.gpg
+
 
 Types: deb
 URIs: https://repo.steampowered.com/steam/
 Suites: stable
 Components: steam
 Architectures: amd64 i386
-Signed-By: /usr/share/keyrings/steam.gpg
+Signed-By: /usr/share/keyrings/repo.steampowered.com_steam_archive_stable_steam.gpg
+
 
 Types: deb
 URIs: https://hub.unity3d.com/linux/repos/deb
 Suites: stable
 Components: main
-Signed-By: /etc/apt/keyrings/unityhub.gpg
+Signed-By: /usr/share/keyrings/hub.unity3d.com_linux_keys_public.gpg
+
 
 Types: deb
 URIs: https://deb.nodesource.com/node_18.x
 Suites: nodistro
 Components: main
 Architectures: amd64
-Signed-By: /etc/apt/keyrings/nodesource.gpg
+Signed-By: /usr/share/keyrings/deb.nodesource.com_gpgkey_nodesource-repo.gpg.gpg
+
+
 {% endif %}
-
 {% if host.data.get('gpu') %}
-Types: deb
-URIs: https://nvidia.github.io/libnvidia-container/stable/deb/$(ARCH)
-Suites: /
-Components: main
-Signed-By: /etc/apt/keyrings/nvidia.gpg
+
+#TODO fix Suites
+
+# Types: deb
+# URIs: https://nvidia.github.io/libnvidia-container/stable/deb/$(ARCH)
+# Suites: /
+# Components: main
+# Signed-By: /etc/apt/keyrings/nvidia.gpg
+
+
 {% endif %}
-
 {% if host.data.get('coral') %}
 Types: deb
 URIs: https://packages.cloud.google.com/apt
 Suites: coral-edgetpu-stable
 Components: main
 Signed-By: /etc/apt/keyrings/coral.gpg
-{% endif %}
 
-{% if host.name == 'pipe' %}
-
-todo convert
 
-# seems stuck on jammy since http://deb.odroid.in/n2/ and https://wiki.odroid.com/odroid-n2/os_images/ubuntu don't have anything newer (2023-12-28)
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://archive.canonical.com/ubuntu jammy partner
-deb [signed-by=/etc/apt/trusted.gpg] http://deb.odroid.in/n2/ jammy main
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy multiverse
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy universe
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-backports main restricted universe multiverse
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-security main restricted
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-security multiverse
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-security universe
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates multiverse
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates universe
-# or, if you have to get this, try: https://keyserver.ubuntu.com/pks/lookup?fingerprint=on&op=index&search=0xABB1931B59A40B968609F153D0392EC59F9583BA
-deb [signed-by=/etc/apt/trusted.gpg] http://ppa.launchpad.net/hardkernel/ppa/ubuntu jammy main
-{% endif %}
-
-{% if 'pi' in host.groups %}
-
-todo convert
-
-deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
-deb http://deb.debian.org/debian-security/ bookworm-security main contrib non-free non-free-firmware
-deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
-{% endif %}
-
+{% endif %}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apt/templates/odroid.sources.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,71 @@
+# written by pyinfra
+
+# seems stuck on jammy since http://deb.odroid.in/n2/ and https://wiki.odroid.com/odroid-n2/os_images/ubuntu don't have anything newer (2023-12-28)
+Types: deb
+URIs: http://deb.odroid.in/n2/
+Suites: jammy
+Components: main
+Signed-By: /etc/apt/trusted.gpg
+
+Types: deb
+URIs: http://ports.ubuntu.com/ubuntu-ports/
+Suites: jammy
+Components: main restricted
+Signed-By: /etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg
+
+Types: deb
+URIs: http://ports.ubuntu.com/ubuntu-ports/
+Suites: jammy
+Components: multiverse
+Signed-By: /etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg
+
+Types: deb
+URIs: http://ports.ubuntu.com/ubuntu-ports/
+Suites: jammy
+Components: universe
+Signed-By: /etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg
+
+Types: deb
+URIs: http://ports.ubuntu.com/ubuntu-ports/
+Suites: jammy-backports
+Components: main restricted universe multiverse
+Signed-By: /etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg
+
+Types: deb
+URIs: http://ports.ubuntu.com/ubuntu-ports/
+Suites: jammy-security
+Components: main restricted
+Signed-By: /etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg
+
+Types: deb
+URIs: http://ports.ubuntu.com/ubuntu-ports/
+Suites: jammy-security
+Components: multiverse
+Signed-By: /etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg
+
+Types: deb
+URIs: http://ports.ubuntu.com/ubuntu-ports/
+Suites: jammy-security
+Components: universe
+Signed-By: /etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg
+
+Types: deb
+URIs: http://ports.ubuntu.com/ubuntu-ports/
+Suites: jammy-updates
+Components: main restricted
+Signed-By: /etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg
+
+Types: deb
+URIs: http://ports.ubuntu.com/ubuntu-ports/
+Suites: jammy-updates
+Components: multiverse
+Signed-By: /etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg
+
+Types: deb
+URIs: http://ports.ubuntu.com/ubuntu-ports/
+Suites: jammy-updates
+Components: universe
+Signed-By: /etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg
+
+# # or, if you have to get this, try: https://keyserver.ubuntu.com/pks/lookup?fingerprint=on&op=index&search=0xABB1931B59A40B968609F153D0392EC59F9583BA
+# deb [signed-by=/etc/apt/trusted.gpg] http://ppa.launchpad.net/hardkernel/ppa/ubuntu jammy main
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apt/templates/pi.sources.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,19 @@
+# written by pyinfra
+
+Types: deb
+URIs: http://deb.debian.org/debian
+Suites: bookworm
+Components: main contrib non-free non-free-firmware
+Signed-By: /usr/share/keyrings/ftp-master.debian.org_keys_archive-key-11.gpg
+
+Types: deb
+URIs: http://deb.debian.org/debian-security/
+Suites: bookworm-security
+Components: main contrib non-free non-free-firmware
+Signed-By: /usr/share/keyrings/ftp-master.debian.org_keys_archive-key-11-security.gpg
+
+Types: deb
+URIs: http://deb.debian.org/debian
+Suites: bookworm-updates
+Components: main contrib non-free non-free-firmware
+Signed-By: /usr/share/keyrings/ftp-master.debian.org_keys_archive-key-11.gpg
--- a/apt/templates/sources.list.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-# written by pyinfra
-
-{% if 'big' in host.groups or 'laptop' in host.groups %}
-deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/ms.gpg] http://packages.microsoft.com/repos/code stable main
-deb [arch=amd64 signed-by=/etc/apt/keyrings/chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main
-deb [arch=amd64,i386 signed-by=/usr/share/keyrings/steam.gpg] https://repo.steampowered.com/steam/ stable steam
-deb [signed-by=/etc/apt/keyrings/unityhub.gpg] https://hub.unity3d.com/linux/repos/deb stable main
-deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main
-{% endif %}
-
-{% if host.data.get('gpu') %}
-deb [signed-by=/etc/apt/keyrings/nvidia.gpg] https://nvidia.github.io/libnvidia-container/stable/deb/$(ARCH) /
-{% endif %}
-
-{% if host.data.get('coral') %}
-deb [signed-by=/etc/apt/keyrings/coral.gpg] https://packages.cloud.google.com/apt coral-edgetpu-stable main
-{% endif %}
-
-{% if host.name == 'pipe' %}
-# seems stuck on jammy since http://deb.odroid.in/n2/ and https://wiki.odroid.com/odroid-n2/os_images/ubuntu don't have anything newer (2023-12-28)
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://archive.canonical.com/ubuntu jammy partner
-deb [signed-by=/etc/apt/trusted.gpg] http://deb.odroid.in/n2/ jammy main
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy multiverse
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy universe
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-backports main restricted universe multiverse
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-security main restricted
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-security multiverse
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-security universe
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates multiverse
-deb [signed-by=/etc/apt/trusted.gpg.d/ubuntu-keyring-2018-archive.gpg] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates universe
-# or, if you have to get this, try: https://keyserver.ubuntu.com/pks/lookup?fingerprint=on&op=index&search=0xABB1931B59A40B968609F153D0392EC59F9583BA
-deb [signed-by=/etc/apt/trusted.gpg] http://ppa.launchpad.net/hardkernel/ppa/ubuntu jammy main
-{% endif %}
-
-{% if 'pi' in host.groups %}
-deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
-deb http://deb.debian.org/debian-security/ bookworm-security main contrib non-free non-free-firmware
-deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
-{% endif %}
-
--- a/coredns_freshen.sh	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-#!/bin/zsh
-kubectl apply -f templates/kube/coredns.yaml
-kubectl get -n kube-system configmap/coredns -o yaml 
--- a/dns.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-import subprocess
-from io import StringIO
-
-import pyinfra
-from pyinfra import host
-from pyinfra.operations import files, server, systemd
-
-
-def dnsmasq_instance(net_name,
-                     house_iface,
-                     dhcp_range='10.2.0.10,10.2.0.11',
-                     listen_address='reqd',
-                     dhcp_hosts_filename='/dev/null'):
-    files.directory(path=f'/opt/dnsmasq/{net_name}')
-    files.template(
-        src='templates/dnsmasq/dnsmasq.conf.j2',
-        dest=f'/opt/dnsmasq/{net_name}/dnsmasq.conf',
-        net=net_name,
-        house_iface=house_iface,
-        dhcp_range=dhcp_range,
-        listen_address=listen_address,
-        dhcp_enabled=net_name == '10.2' and host.name == 'pipe',
-        dns_server=listen_address,
-        router=listen_address,
-    )
-    files.template(src='templates/dnsmasq/hosts.j2', dest=f'/opt/dnsmasq/{net_name}/hosts', net=net_name)
-
-    dhcp_hosts = subprocess.check_output(['python3', '/my/serv/lanscape/src/public/make_dhcp_hosts.py'], encoding='utf8')
-    files.put(src=StringIO(dhcp_hosts), dest=f'/opt/dnsmasq/{net_name}/dhcp_hosts')
-
-    files.template(src='templates/dnsmasq/dnsmasq.service.j2',
-                   dest=f'/etc/systemd/system/dnsmasq_{net_name}.service',
-                   net=net_name)
-    if net_name in ['10.2', '10.2-filtered']:
-        systemd.service(service=f'dnsmasq_{net_name}', enabled=True, restarted=True, daemon_reload=True)
-
-
-def standard_host_dns():
-    files.template(src='templates/hosts.j2', dest='/etc/hosts')
-    if 'pi' in host.groups:
-        files.put(dest='/etc/resolv.conf',
-                  src=StringIO('''
-# written by pyinfra
-nameserver 10.2.0.3
-search bigasterisk.com
-                  '''))
-    else:
-        files.link(path='/etc/resolv.conf', target='/run/systemd/resolve/resolv.conf', force=True)
-        files.template(src='templates/resolved.conf.j2', dest='/etc/systemd/resolved.conf')
-        systemd.service(service='systemd-resolved.service', running=True, restarted=True)
-
-
-def rpi_net_boot():
-    files.directory(path='/opt/dnsmasq/tftp')
-
-
-standard_host_dns()
-
-# no default instance; i'll add some specific ones below
-systemd.service(service='dnsmasq', enabled=False, running=False)
-
-
-def watchLeasesFile():
-    """summary:
-    1. dnsmasq_10.2 leases an address and writes to /opt/dnsmasq/10.2/leases
-    2. dhcp_graph_watch.path notices that change
-    3. dhcp_graph_update.service posts /opt/dnsmasq/10.2/leases to dhcp_graph (k8s deploy)
-    4. dhcp_graph serves the data as rdf
-    """
-    dhcp_graph_url = "http://10.5.0.7:8005"
-    leases = "/opt/dnsmasq/10.2/leases"
-    files.put(dest='/etc/systemd/system/dhcp_graph_watch.path',
-              src=StringIO(f'''
-[Unit]
-Description=dhcp leases file changed- run dhcp_graph_update
-After=localfs.target
-
-[Path]
-PathModified={leases}
-Unit=dhcp_graph_update.service
-
-[Install]
-WantedBy=multi-user.target
-'''))
-
-    files.put(dest='/etc/systemd/system/dhcp_graph_update.service',
-              src=StringIO(f'''
-[Unit]
-Description=Send new dhcp leases content to dhcp_graph
-After=network.target
-
-[Service]
-Type=oneshot
-ExecStart=/usr/bin/curl -s {dhcp_graph_url}/leases -H "content-type: text/plain" --data-binary "@{leases}"
-
-[Install]
-WantedBy=multi-user.target
-'''))
-    systemd.service(service='dhcp_graph_watch.path', enabled=True, restarted=True, daemon_reload=True)
-    systemd.service(service='dhcp_graph_update.service', enabled=True, restarted=True, daemon_reload=True)
-
-
-if host.name == 'pipe':
-    rpi_net_boot()
-    files.directory(path='/opt/dnsmasq')
-    dnsmasq_instance('10.2',
-                     house_iface='eth1',
-                     dhcp_range='10.2.0.110,10.2.0.240',
-                     listen_address='10.2.0.3',
-                     dhcp_hosts_filename='templates/dnsmasq/dhcp_hosts.j2')
-    out = '/opt/dnsmasq/10.2'
-    # This mtail is for dhcp command counts and errors.
-    files.put(src='files/dnsmasq/metrics.mtail', dest=f'{out}/metrics.mtail')
-    files.put(src='files/dnsmasq/run_mtail.sh', dest=f'{out}/run_mtail.sh')
-
-    watchLeasesFile()
-
-    files.put(src='files/dnsmasq/dnsmasq-mtail.service', dest='/etc/systemd/system/dnsmasq-mtail.service')
-    systemd.service(service='dnsmasq-mtail', enabled=True, restarted=True, daemon_reload=True)
-
-    # Serve another dns, no dhcp, and include the dynamic-blocking file written by net_routes.
-    dnsmasq_instance(
-        net_name='10.2-filtered',
-        house_iface='eth1',
-        listen_address='10.2.0.4',
-    )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dns/dns.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,116 @@
+from operator import le
+import subprocess
+from io import StringIO
+
+from pyinfra.context import host
+from pyinfra.operations import files, systemd
+
+
+def dnsmasq_instance(
+    net_name,
+    house_iface,
+    dhcp_range='10.2.0.10,10.2.0.11',
+    listen_address='reqd',
+):
+    files.directory(path=f'/opt/dnsmasq/{net_name}')
+    files.template(
+        src='dns/templates/dnsmasq/dnsmasq.conf.j2',
+        dest=f'/opt/dnsmasq/{net_name}/dnsmasq.conf',
+        net=net_name,
+        house_iface=house_iface,
+        dhcp_range=dhcp_range,
+        listen_address=listen_address,
+        dhcp_enabled=net_name == '10.2' and host.name == 'pipe',
+        dns_server=listen_address,
+        router=listen_address,
+    )
+    files.template(src='dns/templates/dnsmasq/hosts.j2', dest=f'/opt/dnsmasq/{net_name}/hosts', net=net_name)
+
+    dhcp_hosts = subprocess.check_output(['python3', '/my/serv/lanscape/src/public/make_dhcp_hosts.py'], encoding='utf8')
+    files.put(src=StringIO(dhcp_hosts), dest=f'/opt/dnsmasq/{net_name}/dhcp_hosts')
+
+    files.template(src='dns/templates/dnsmasq/dnsmasq.service.j2',
+                   dest=f'/etc/systemd/system/dnsmasq_{net_name}.service',
+                   net=net_name)
+    if net_name in ['10.2', '10.2-filtered']:
+        systemd.service(service=f'dnsmasq_{net_name}', enabled=True, restarted=True, daemon_reload=True)
+
+
+def standard_host_dns():
+    files.template(src='dns/templates/hosts.j2', dest='/etc/hosts')
+    if 'pi' in host.groups:
+        files.put(dest='/etc/resolv.conf', src='dns/files/resolv.conf')
+    else:
+        files.link(path='/etc/resolv.conf', target='/run/systemd/resolve/resolv.conf', force=True)
+        files.template(src='dns/templates/resolved.conf.j2', dest='/etc/systemd/resolved.conf')
+        systemd.service(service='systemd-resolved.service', running=True, restarted=True)
+
+
+def rpi_net_boot():
+    files.directory(path='/opt/dnsmasq/tftp')
+
+
+def no_default_dnsmasq_instance():
+    # no default instance; i'll add some specific ones below
+    systemd.service(service='dnsmasq', enabled=False, running=False)
+
+
+def watchLeasesFile():
+    """summary:
+    1. dnsmasq_10.2 leases an address and writes to /opt/dnsmasq/10.2/leases
+    2. dhcp_graph_watch.path notices that change
+    3. dhcp_graph_update.service posts /opt/dnsmasq/10.2/leases to dhcp_graph (k8s deploy)
+    4. dhcp_graph serves the data as rdf
+    """
+    dhcp_graph_url = "http://10.5.0.7:8005"
+    leases = "/opt/dnsmasq/10.2/leases"
+    files.template(
+        src='dns/templates/dhcp_graph_watch.path.j2',
+        dest='/etc/systemd/system/dhcp_graph_watch.path',
+        leases=leases,
+    )
+
+    files.template(
+        src='dns/templates/dhcp_graph_update.service.j2',
+        dest='/etc/systemd/system/dhcp_graph_update.service',
+        leases=leases,
+        dhcp_graph_url=dhcp_graph_url,
+    )
+    systemd.service(service='dhcp_graph_watch.path', enabled=True, restarted=True, daemon_reload=True)
+    systemd.service(service='dhcp_graph_update.service', enabled=True, restarted=True, daemon_reload=True)
+
+
+def dnsmasq_on_pipe():
+    if host.name != 'pipe':
+        return
+    rpi_net_boot()
+    files.directory(path='/opt/dnsmasq')
+    dnsmasq_instance(
+        '10.2',
+        house_iface='eth1',
+        dhcp_range='10.2.0.110,10.2.0.240',
+        listen_address='10.2.0.3',
+    )
+    out = '/opt/dnsmasq/10.2'
+    # This mtail is for dhcp command counts and errors.
+    files.put(src='dns/files/metrics.mtail', dest=f'{out}/metrics.mtail')
+    files.put(src='dns/files/run_mtail.sh', dest=f'{out}/run_mtail.sh')
+
+    watchLeasesFile()
+
+    files.put(src='dns/files/dnsmasq-mtail.service', dest='/etc/systemd/system/dnsmasq-mtail.service')
+    systemd.service(service='dnsmasq-mtail', enabled=True, restarted=True, daemon_reload=True)
+
+    # Serve another dns, no dhcp, and include the dynamic-blocking file written by net_routes.
+    dnsmasq_instance(
+        net_name='10.2-filtered',
+        house_iface='eth1',
+        listen_address='10.2.0.4',
+    )
+
+
+operations = [
+    standard_host_dns,
+    no_default_dnsmasq_instance,
+    dnsmasq_on_pipe,
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dns/files/dnsmasq-mtail.service	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,13 @@
+# written by pyinfra
+
+[Unit]
+Description=dnsmasq-mtail for 10.2 network
+After=dnsmasq_10.2.service
+
+[Service]
+Type=simple
+
+ExecStart=zsh /opt/dnsmasq/10.2/run_mtail.sh
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dns/files/metrics.mtail	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,37 @@
+counter dnsmasq_no_addr_errors
+/no address available/ {
+    dnsmasq_no_addr_errors++
+}
+
+counter dnsmasq_dhcp_requests
+/DHCPREQUEST/ {
+    dnsmasq_dhcp_requests++
+}
+
+counter dnsmasq_dhcp_acks
+/DHCPACK/ {
+    dnsmasq_dhcp_acks++
+}
+
+counter dnsmasq_dhcp_discovers
+/DHCPDISCOVER/ {
+    dnsmasq_dhcp_discovers++
+}
+
+counter dnsmasq_dhcp_offers
+/DHCPOFFER/ {
+    dnsmasq_dhcp_offers++
+}
+
+gauge dnsmasq_dns_queries_answered_locally
+gauge dnsmasq_dns_queries_forwarded by server
+gauge dnsmasq_dns_queries_retried_or_failed by server
+
+/queries forwarded (?P<fwd>\d+), queries answered locally (?P<loc>\d+)/ {
+    dnsmasq_dns_queries_answered_locally = $loc
+}
+
+/server (?P<svr>\S+)#53: queries sent (?P<sent>\d+), retried or failed (?P<fail>\d+)/ {
+    dnsmasq_dns_queries_forwarded[$svr] = $sent
+    dnsmasq_dns_queries_retried_or_failed[$svr] = $fail
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dns/files/resolv.conf	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,6 @@
+# written by pyinfra
+
+# (used on rpi)
+
+nameserver 10.2.0.3
+search bigasterisk.com
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dns/files/run_mtail.sh	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,11 @@
+#!/bin/zsh
+STATS_PERIOD=2m
+while (true) { pkill --signal USR1 --oldest --full /usr/sbin/dnsmasq; sleep ${STATS_PERIOD} } &
+
+rm -f /tmp/dnsmasq_log_pipe
+mkfifo /tmp/dnsmasq_log_pipe
+
+{ journalctl -fu dnsmasq_10.2.service > /tmp/dnsmasq_log_pipe } &
+
+mtail -port 9991 -logtostderr -logs /tmp/dnsmasq_log_pipe  -progs /opt/dnsmasq/10.2
+#-disable_fsnotify -poll_interval ${STATS_PERIOD}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dns/templates/dhcp_graph_update.service.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,10 @@
+[Unit]
+Description=Send new dhcp leases content to dhcp_graph
+After=network.target
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/curl -s {{dhcp_graph_url}}/leases -H "content-type: text/plain" --data-binary "@{{leases}}"
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dns/templates/dhcp_graph_watch.path.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,10 @@
+[Unit]
+Description=dhcp leases file changed- run dhcp_graph_update
+After=localfs.target
+
+[Path]
+PathModified={{leases}}
+Unit=dhcp_graph_update.service
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dns/templates/dnsmasq/dnsmasq.conf.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,71 @@
+user=nobody
+keep-in-foreground
+log-facility=-
+
+listen-address={{ listen_address }}
+{% if net == "10.2" %}
+# dnsmasq will not automatically listen on the loopback interface. To achieve
+# this, its IP address, 127.0.0.1, must be explicitly given as a
+# --listen-address option.
+listen-address=127.0.0.1
+{% endif %}
+bind-interfaces
+
+domain-needed
+no-resolv
+no-hosts
+addn-hosts=/opt/dnsmasq/{{ net }}/hosts
+local-ttl=30
+mx-host=bigasterisk.com
+cache-size=10000
+neg-ttl=60
+dns-forward-max=1000
+domain=bigasterisk.com
+
+# log-queries
+# log-debug
+
+{% if dhcp_enabled %}
+log-dhcp
+
+dhcp-sequential-ip
+dhcp-broadcast
+dhcp-authoritative
+dhcp-option=option:domain-name,bigasterisk.com
+dhcp-script=/opt/dnsmasq_exporter/on_dhcp_change.sh
+dhcp-hostsfile=/opt/dnsmasq/{{ net }}/dhcp_hosts
+dhcp-leasefile=/opt/dnsmasq/{{ net }}/leases
+dhcp-range={{ house_iface }},10.2.0.0,static,infinite
+dhcp-range=tag:!known,{{ house_iface }},{{ dhcp_range }},2h
+dhcp-option={{ house_iface }},option:dns-server,{{ dns_server }}
+dhcp-option={{ house_iface }},option:router,{{ router }}
+# hosts are tagged in ./dhcp_hosts.j2
+dhcp-option=tag:filtereddns,option:dns-server,10.2.0.4
+
+enable-tftp
+tftp-root=/opt/dnsmasq/tftp
+pxe-service=0,"Raspberry Pi Boot"
+ dhcp-mac=set:net-booting-rpi,b8:27:eb:*:*:*
+ dhcp-reply-delay=tag:net-booting-rpi,2
+{% endif %}
+
+local=/bigasterisk.com/
+# i didn't say --all-servers, but it was behaving like that
+server=208.201.224.11
+#server=208.201.224.33
+#server=8.8.4.4
+#server=8.8.8.8
+
+{% if net == "10.5" %}
+# net==10.5 is not used for dhcp at all
+# use ./hosts, then try the server that knows the dhcp leases
+server={{ router }}
+{% endif %}
+
+{% if net == '10.2-filtered' %}
+# written by net_routes/dns_blocker.py
+addn-hosts=/opt/dnsmasq/10.2-filtered/dynamic-blocking
+# but! users of this dns server can't even look up names 
+# like 'ditto' since those come from dhcp on the 10.2.0.3
+# (nonfiltered) dnsmasq instance
+{% endif %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dns/templates/dnsmasq/dnsmasq.service.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,52 @@
+# written by pyinfra
+
+[Unit]
+Description=dnsmasq for {{ net }} network
+
+# this dnsmasq needs to bind to addr 10.2.0.3
+Requires=network-online.target
+Requires=sys-subsystem-net-devices-eth1.device
+
+Wants=nss-lookup.target
+Before=nss-lookup.target
+After=network.target
+
+# startup order has to be like this:
+#    dnsmasq_10.2
+#    wg-quick@wg0.service
+#    dnsmasq_10.5
+{% if net == '10.2' %}
+Before=wg-quick@wg0.service
+After=house_net.service
+{% endif %}
+{% if net == '10.5' %}
+Requires=wg-quick@wg0.service
+{% endif %}
+
+[Service]
+Type=simple
+
+# 10.5 will not work until wg0 interface is actually up, so just let it retry
+# but i think this next line was not the right way to retry.
+#SuccessExitStatus=2
+Restart=always
+RestartSec=5
+
+# Test the config file and refuse starting if it is not valid.
+ExecStartPre=/usr/sbin/dnsmasq --conf-file=/opt/dnsmasq/{{ net }}/dnsmasq.conf --test
+
+ExecStart=/usr/sbin/dnsmasq --conf-file=/opt/dnsmasq/{{ net }}/dnsmasq.conf 
+
+{% if net == '10.2' %}
+# The systemd-*-resolvconf functions configure (and deconfigure)
+# resolvconf to work with the dnsmasq DNS server. They're called like
+# this to get correct error handling (ie don't start-resolvconf if the 
+# dnsmasq daemon fails to start.
+ExecStartPost=/etc/init.d/dnsmasq systemd-start-resolvconf
+ExecStop=/etc/init.d/dnsmasq systemd-stop-resolvconf
+{% endif %}
+
+ExecReload=/bin/kill -HUP $MAINPID
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dns/templates/dnsmasq/hosts.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,60 @@
+# written by pyinfra
+
+162.243.138.136 prime-ext.bigasterisk.com public.bigasterisk.com
+
+# This is the dns trick-- hosts at home should use the local address
+# for 'bigasterisk.com' etc instead of taking a trip to prime.
+10.2.0.1 bang bang.bigasterisk.com 
+10.2.0.133 bigasterisk.com cam-int.bigasterisk.com cam-ext.bigasterisk.com imap.bigasterisk.com repo.bigasterisk.com drewp.quickwitretort.com photo.bigasterisk.com projects.bigasterisk.com quickwitretort.com whatsplayingnext.com whopickedthis.com vpn-home.bigasterisk.com file.bigasterisk.com antigen-superset.bigasterisk.com authenticate.bigasterisk.com authenticate2.bigasterisk.com authenticate3.bigasterisk.com megasecond.club hass.bigasterisk.com bitwarden.bigasterisk.com livegrep.bigasterisk.com dev.bigasterisk.com apprise.bigasterisk.com sco-bot-prefect.bigasterisk.com paperless.bigasterisk.com linkwarden.bigasterisk.com jellyfin.bigasterisk.com viseron.bigasterisk.com chat.bigasterisk.com
+
+# deleteme
+162.243.138.136 light9.bigasterisk.com
+
+# VIPs on ditto
+10.2.0.11 mqtt1 mqtt1.bigasterisk.com
+10.2.0.12 mqtt2 mqtt2.bigasterisk.com
+# might be used for syncthing
+10.2.0.13 mqtt3 mqtt3.bigasterisk.com 
+10.2.0.14 mqtt4 mqtt4.bigasterisk.com
+
+10.2.0.15 victorialogs.bigasterisk.com
+
+# sync with /my/proj/infra/inventory.py
+# and with templates/wireguard/wg0.conf.j2
+# Hosts with fixed wg0 addresses:
+10.5.0.1   bang5.bigasterisk.com local.bigasterisk.com reg 
+10.5.0.2   prime5.bigasterisk.com prime.bigasterisk.com 
+10.5.0.5   dash5.bigasterisk.com
+10.5.0.6   slash5.bigasterisk.com
+10.5.0.7   ditto5.bigasterisk.com
+10.5.0.14  ga-iot5.bigasterisk.com
+10.5.0.17  frontbed5.bigasterisk.com
+10.5.0.30  dot5.bigasterisk.com
+10.5.0.31  ws-printer5.bigasterisk.com
+10.5.0.32  gn-music5.bigasterisk.com
+10.5.0.33  li-drums5.bigasterisk.com
+10.5.0.110 plus5.bigasterisk.com
+10.5.0.111 pillow.bigasterisk.com pillow5.bigasterisk.com
+10.5.0.112 drew-note5.bigasterisk.com
+10.5.0.113 tofu.bigasterisk.com tofu5.bigasterisk.com
+
+{% if net == '10.2' %}
+# Hosts with fixed addrs who don't introduce via dhcp:
+# 162.243.138.136   prime.bigasterisk.com
+10.2.0.3 pipe pipe.bigasterisk.com
+# from netdevices.n3
+10.2.0.133 ditto ditto.bigasterisk.com
+{% endif %}
+
+{% if net == '10.5' %}
+# Names that should be routed on wg0 when the DNS lookup is on wg0:
+10.5.0.1   bang.bigasterisk.com
+10.5.0.5   dash.bigasterisk.com
+10.5.0.6   slash.bigasterisk.com
+10.5.0.7   ditto.bigasterisk.com
+10.5.0.14  ga-iot.bigasterisk.com
+10.5.0.17  frontbed.bigasterisk.com
+10.5.0.30  dot.bigasterisk.com
+10.5.0.110 plus.bigasterisk.com
+10.5.0.112 drew-note.bigasterisk.com
+{% endif %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dns/templates/hosts.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,23 @@
+# written by pyinfra
+
+127.0.0.1       localhost
+127.0.1.1       {{ host.name }}
+
+# The following lines are desirable for IPv6 capable hosts
+::1     ip6-localhost ip6-loopback
+fe00::0 ip6-localnet
+ff00::0 ip6-mcastprefix
+ff02::1 ip6-allnodes
+ff02::2 ip6-allrouters
+
+
+{% if 'laptop' in host.groups or 'hosted' in host.groups %}
+10.5.0.1 bang bang.bigasterisk.com bang5 bang5.bigasterisk.com 
+10.5.0.7 ditto ditto.bigasterisk.com ditto5 ditto5.bigasterisk.com 
+10.5.0.5 dash
+{% endif %}
+
+{% if host.name == 'prime' %}
+# for wireguard setup:
+127.0.0.1 public.bigasterisk.com
+{% endif %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dns/templates/resolved.conf.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,25 @@
+# written by pyinfra
+
+# See resolved.conf(5) for details
+
+{% if host.name == 'prime' %}
+[Resolve]
+# This list is tried randomly, not in order, so we could have
+# some trouble with internal names
+DNS=10.5.0.1 8.8.8.8 8.8.4.4
+Domains=bigasterisk.com
+
+{% else %}
+[Resolve]
+# worst case- you might get a better one over DHCP, which would get listed AFTER this one so it needs to be the only one.
+#DNS=10.2.0.4
+#FallbackDNS=
+Domains=bigasterisk.com
+#LLMNR=no
+#MulticastDNS=no
+#DNSSEC=no
+#DNSOverTLS=no
+#Cache=yes
+#DNSStubListener=yes
+#ReadEtcHosts=yes
+{% endif %}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dnsmasq_exporter.service	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,15 @@
+unused?
+
+# written by pyinfra
+
+[Unit]
+Description=send updated leases to dnsmasq_exporter
+After=dnsmasq_10.2.service
+
+[Service]
+Type=simple
+
+ExecStart=zsh -c 'print /opt/dnsmasq/10.2/leases | entr curl http://10.5.0.7:9998/leases -H "content-type: text/plain" -d "@/opt/dnsmasq/10.2/leases"'
+
+[Install]
+WantedBy=multi-user.target
--- a/files/dnsmasq/dnsmasq-mtail.service	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-# written by pyinfra
-
-[Unit]
-Description=dnsmasq-mtail for 10.2 network
-After=dnsmasq_10.2.service
-
-[Service]
-Type=simple
-
-ExecStart=zsh /opt/dnsmasq/10.2/run_mtail.sh
-
-[Install]
-WantedBy=multi-user.target
--- a/files/dnsmasq/metrics.mtail	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-counter dnsmasq_no_addr_errors
-/no address available/ {
-    dnsmasq_no_addr_errors++
-}
-
-counter dnsmasq_dhcp_requests
-/DHCPREQUEST/ {
-    dnsmasq_dhcp_requests++
-}
-
-counter dnsmasq_dhcp_acks
-/DHCPACK/ {
-    dnsmasq_dhcp_acks++
-}
-
-counter dnsmasq_dhcp_discovers
-/DHCPDISCOVER/ {
-    dnsmasq_dhcp_discovers++
-}
-
-counter dnsmasq_dhcp_offers
-/DHCPOFFER/ {
-    dnsmasq_dhcp_offers++
-}
-
-gauge dnsmasq_dns_queries_answered_locally
-gauge dnsmasq_dns_queries_forwarded by server
-gauge dnsmasq_dns_queries_retried_or_failed by server
-
-/queries forwarded (?P<fwd>\d+), queries answered locally (?P<loc>\d+)/ {
-    dnsmasq_dns_queries_answered_locally = $loc
-}
-
-/server (?P<svr>\S+)#53: queries sent (?P<sent>\d+), retried or failed (?P<fail>\d+)/ {
-    dnsmasq_dns_queries_forwarded[$svr] = $sent
-    dnsmasq_dns_queries_retried_or_failed[$svr] = $fail
-}
\ No newline at end of file
--- a/files/dnsmasq/run_mtail.sh	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-#!/bin/zsh
-STATS_PERIOD=2m
-while (true) { pkill --signal USR1 --oldest --full /usr/sbin/dnsmasq; sleep ${STATS_PERIOD} } &
-
-rm -f /tmp/dnsmasq_log_pipe
-mkfifo /tmp/dnsmasq_log_pipe
-
-{ journalctl -fu dnsmasq_10.2.service > /tmp/dnsmasq_log_pipe } &
-
-mtail -port 9991 -logtostderr -logs /tmp/dnsmasq_log_pipe  -progs /opt/dnsmasq/10.2
-#-disable_fsnotify -poll_interval ${STATS_PERIOD}
\ No newline at end of file
--- a/files/fstab/bang	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-# written by pyinfra
-
-# <file system> <mount point>   <type>  <options>       <dump>  <pass>
-/dev/disk/by-uuid/8c7a2d08-60d1-486a-8136-d9f43d83a064 / ext4 relatime 0 0
-/dev/disk/by-uuid/d9a1e1e4-9eba-4988-8b01-c5f6732a2972 /d3 ext4 noatime 0 0
-/dev/disk/by-partuuid/77687eec-15bf-9345-b420-bb83659e6a6b /d4 ext4 noatime 0 0
-
-ditto5:/my                           /my       nfs  rw,noatime          0       0
--- a/files/fstab/dash	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-# written by pyinfra
-
-# <file system> <mount point>   <type>  <options>       <dump>  <pass>
-/dev/disk/by-uuid/d8d23ff1-7c37-4a7d-9fc4-55fc61f912a0 / ext4 defaults 0 1
-/dev/disk/by-uuid/CB55-821E /boot/efi vfat defaults 0 1
-
-UUID=73bcd201-5f77-4f68-9fba-47835c3c1692 /d2  ext4  defaults 0 0
-UUID=6cae1c30-3c91-4aa7-9e9f-fcbd7ff706fe /d3  ext4  defaults 0 0
-UUID=3b6780e0-ec86-43be-8d09-e462dbad762e /d4  ext4  defaults 0 0
-
-ditto5:/my                                /my  nfs   rw,noatime 0 0
--- a/files/fstab/ditto	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-# written by pyinfra
-
-
-#
-# Use 'blkid' to print the universally unique identifier for a
-# device; this may be used with UUID= as a more robust way to name devices
-# that works even if disks are added and removed. See fstab(5).
-#
-# <file system> <mount point>   <type>  <options>       <dump>  <pass>
-/dev/disk/by-uuid/6e64ce62-34db-4084-9385-d001e99ad38b / ext4 defaults 0 1
-/dev/disk/by-uuid/3F95-42F4 /boot/efi vfat defaults 0 1
-/dev/disk/by-uuid/74795c77-ed20-417d-988a-abc09c3dfc27 /d2 ext4 defaults 0 1
-
-
--- a/files/fstab/dot	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-# written by pyinfra
-
-# <file system> <mount point>   <type>  <options>       <dump>  <pass>
-
-
-
-/dev/disk/by-uuid/a9403f0b-aa16-4096-ab0d-2e2069d3f18a / ext4 defaults 0 1
-
-/dev/mapper/ubuntu--vg-ubuntu--lv                      /d2 ext4 defaults 0 1
-/dev/disk/by-uuid/5a6ce8db-cde0-4c26-b6a4-08faef2e01a2 /d3 ext4 defaults 0 1
--- a/files/fstab/slash	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-# written by pyinfra
-
-# <file system> <mount point>   <type>  <options>       <dump>  <pass>
-UUID=df079890-9431-4e17-940c-d9ed8ce4e149 /         ext4 errors=remount-ro 0       1
-UUID=1CFA-995B                            /boot/efi vfat umask=0077        0       1
-
-ditto5:/my                                /my       nfs  rw,noatime        0       0
--- a/files/fstab/tofu	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-# written by pyinfra
-
-# <file system> <mount point>   <type>  <options>       <dump>  <pass>
-/dev/nvme0n1p6 / ext4 rw,relatime 0 0
--- a/files/kube/k3s-killall.sh	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-#!/bin/sh
-[ $(id -u) -eq 0 ] || exec sudo $0 $@
-
-for bin in /var/lib/rancher/k3s/data/**/bin/; do
-    [ -d $bin ] && export PATH=$PATH:$bin:$bin/aux
-done
-
-set -x
-
-for service in /etc/systemd/system/k3s*.service; do
-    [ -s $service ] && systemctl stop $(basename $service)
-done
-
-for service in /etc/init.d/k3s*; do
-    [ -x $service ] && $service stop
-done
-
-pschildren() {
-    ps -e -o ppid= -o pid= | \
-    sed -e 's/^\s*//g; s/\s\s*/\t/g;' | \
-    grep -w "^$1" | \
-    cut -f2
-}
-
-pstree() {
-    for pid in $@; do
-        echo $pid
-        for child in $(pschildren $pid); do
-            pstree $child
-        done
-    done
-}
-
-killtree() {
-    kill -9 $(
-        { set +x; } 2>/dev/null;
-        pstree $@;
-        set -x;
-    ) 2>/dev/null
-}
-
-getshims() {
-    ps -e -o pid= -o args= | sed -e 's/^ *//; s/\s\s*/\t/;' | grep -w 'k3s/data/[^/]*/bin/containerd-shim' | cut -f1
-}
-
-killtree $({ set +x; } 2>/dev/null; getshims; set -x)
-
-do_unmount_and_remove() {
-    set +x
-    while read -r _ path _; do
-        case "$path" in $1*) echo "$path" ;; esac
-    done < /proc/self/mounts | sort -r | xargs -r -t -n 1 sh -c 'umount "$0" && rm -rf "$0"'
-    set -x
-}
-
-do_unmount_and_remove '/run/k3s'
-do_unmount_and_remove '/var/lib/rancher/k3s'
-do_unmount_and_remove '/var/lib/kubelet/pods'
-do_unmount_and_remove '/var/lib/kubelet/plugins'
-do_unmount_and_remove '/run/netns/cni-'
-
-# Remove CNI namespaces
-ip netns show 2>/dev/null | grep cni- | xargs -r -t -n 1 ip netns delete
-
-# Delete network interface(s) that match 'master cni0'
-ip link show 2>/dev/null | grep 'master cni0' | while read ignore iface ignore; do
-    iface=${iface%%@*}
-    [ -z "$iface" ] || ip link delete $iface
-done
-ip link delete cni0
-ip link delete flannel.1
-ip link delete flannel-v6.1
-ip link delete kube-ipvs0
-rm -rf /var/lib/cni/
-iptables-save | grep -v KUBE- | grep -v CNI- | grep -v flannel | iptables-restore
-ip6tables-save | grep -v KUBE- | grep -v CNI- | grep -v flannel | ip6tables-restore
--- a/files/kube/k3s-uninstall.sh	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-#!/bin/sh
-set -x
-[ $(id -u) -eq 0 ] || exec sudo $0 $@
-
-/usr/local/bin/k3s-killall.sh
-
-if command -v systemctl; then
-    systemctl disable k3s
-    systemctl reset-failed k3s
-    systemctl daemon-reload
-fi
-if command -v rc-update; then
-    rc-update delete k3s default
-fi
-
-rm -f /etc/systemd/system/k3s.service
-rm -f /etc/systemd/system/k3s.service.env
-
-remove_uninstall() {
-    rm -f /usr/local/bin/k3s-uninstall.sh
-}
-trap remove_uninstall EXIT
-
-if (ls /etc/systemd/system/k3s*.service || ls /etc/init.d/k3s*) >/dev/null 2>&1; then
-    set +x; echo 'Additional k3s services installed, skipping uninstall of k3s'; set -x
-    exit
-fi
-
-for cmd in kubectl crictl ctr; do
-    if [ -L /usr/local/bin/$cmd ]; then
-        rm -f /usr/local/bin/$cmd
-    fi
-done
-
-rm -rf /etc/rancher/k3s
-rm -rf /run/k3s
-rm -rf /run/flannel
-rm -rf /var/lib/rancher/k3s
-rm -rf /var/lib/kubelet
-rm -f /usr/local/bin/k3s
-rm -f /usr/local/bin/k3s-killall.sh
-
-if type yum >/dev/null 2>&1; then
-    yum remove -y k3s-selinux
-    rm -f /etc/yum.repos.d/rancher-k3s-common*.repo
-elif type zypper >/dev/null 2>&1; then
-    uninstall_cmd="zypper remove -y k3s-selinux"
-    if [ "${TRANSACTIONAL_UPDATE=false}" != "true" ] && [ -x /usr/sbin/transactional-update ]; then
-        uninstall_cmd="transactional-update --no-selfupdate -d run $uninstall_cmd"
-    fi
-    $uninstall_cmd
-    rm -f /etc/zypp/repos.d/rancher-k3s-common*.repo
-fi
--- a/files/kube/kubelet.config	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-apiVersion: kubelet.config.k8s.io/v1beta1
-kind: KubeletConfiguration
-maxPods: 250
--- a/files/net/ditto-netplan.yaml	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-# written by pyinfra
-network:
-  ethernets:
-    eno1:
-      addresses:
-        # name=ditto
-        - 10.2.0.133/16
-        # name=mqtt1 etc
-        - 10.2.0.11/16
-        - 10.2.0.12/16
-        - 10.2.0.13/16
-        - 10.2.0.14/16
-      routes:
-        - to: default
-          via: 10.2.0.3
-      nameservers:
-        addresses: [10.2.0.3]
-    enp5s0:
-      dhcp4: true
-    ens3:
-      dhcp4: true
-  version: 2
--- a/files/net/house_net.service	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-# written by pyinfra
-
-[Unit]
-After=network-online.target nss-lookup.target
-Wants=network-online.target nss-lookup.target
-
-[Service]
-Type=oneshot
-ExecStart=sh -c "sysctl net.ipv4.ip_forward=1 && /usr/sbin/iptables -A POSTROUTING --table nat --out-interface eth0 --jump MASQUERADE"
-RemainAfterExit=yes
-
-
-[Install]
-WantedBy=multi-user.target
--- a/files/net/pipe_10.2.network	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-# written by pyinfra
-
-[Match]
-# usb dongle
-MACAddress=00:05:1b:33:3e:81
-
-[Network]
-DHCP=no
-Address=10.2.0.3/16
-# vip for the filtered dns server we give clients who are to have sometimes-filtered domains
-Address=10.2.0.4/16
-DNS=10.2.0.3
-Domains=bigasterisk.com
-
--- a/files/net/pipe_isp.network	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-# written by pyinfra
-
-[Match]
-# onboard eth
-MACAddress=00:1e:06:43:20:d0
-
-[Network]
-DHCP=no
-Address=192.168.42.3/24
-Gateway=192.168.42.1
-DNS=10.2.0.1
-Domains=bigasterisk.com
\ No newline at end of file
--- a/files/net/prime.network	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-# written by pyinfra
-
-# see systemd.network(5)
-
-[Match]
-MACAddress=04:01:09:7f:89:01
-
-[Network]
-Address=162.243.138.136/24
-Gateway=162.243.138.1
-DNS=10.5.0.1%wg0
-DNS=8.8.8.8
-DNS=8.8.4.4
-Domains=bigasterisk.com
\ No newline at end of file
--- a/files/net/singlenic.network	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-# written by pyinfra
-
-[Match]
-Name=*
-
-[Network]
-DHCP=yes
-# this sauce may or may not help with k3s
-LinkLocalAddressing=yes
-IPForward=yes
\ No newline at end of file
--- a/files/pi_wlan0_powersave	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-# written by pyinfra
-
-auto wlan0
-iface wlan0 inet dhcp
-  post-up iw wlan0 set power_save off
--- a/files/pigpiod.service	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-# written by pyinfra
-
-[Unit]
-Description=Daemon required to control GPIO pins via pigpio
-
-[Service]
-Type=simple
-ExecStart=/usr/bin/pigpiod -g -p 8888
-
-[Install]
-WantedBy=multi-user.target
--- a/home.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-from pyinfra import host
-from pyinfra.operations import files, server
-
-if host.data.get('drewp_home'):
-    # maybe bring sync.py in here too
-
-    server.shell(commands=['chsh -s /bin/zsh drewp'])
-    files.link(path='/home/drewp/.aptitude/config', target='../own/config/aptitude-config', force=True)
-    files.link(path='/home/drewp/.config/blender',  target='../own/config/blender', force=True)
-    files.link(path='/home/drewp/.config/i3',       target='../own/config/i3', force=True)
-    files.link(path='/home/drewp/.emacs.d',         target='own/config/emacs-d', force=True)
-    files.link(path='/home/drewp/.fonts',           target='own/config/fonts', force=True)
-    files.link(path='/home/drewp/.fvwm2rc',         target='own/config/fvwm2rc', force=True)
-    files.link(path='/home/drewp/.hgrc',            target='own/config/hgrc', force=True)
-    files.link(path='/home/drewp/.kitty',           target='own/config/kitty', force=True)
-    files.link(path='/home/drewp/.zshrc',           target='own/config/zshrc', force=True)
-    files.link(path='/home/drewp/bin',              target='own/config/bin/', force=True)
-    files.link(path='/home/drewp/blenderkit_data',  target='own/gfx-lib/blenderkit_data/', force=True)
-
-#drwx------  3 drewp drewp  4096 Jul 31 15:07 .config/syncthing
-#npm.rc?
-# run on bang: pnpm server --background start
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/home/home.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,27 @@
+from pyinfra.context import host
+from pyinfra.operations import files, server
+
+def drewp_home_sync_links():
+    if host.data.get('drewp_home'):
+        # maybe bring sync.py in here too
+
+        server.shell(commands=['chsh -s /bin/zsh drewp'])
+        files.link(path='/home/drewp/.aptitude/config', target='../own/config/aptitude-config', force=True)
+        files.link(path='/home/drewp/.config/blender',  target='../own/config/blender', force=True)
+        files.link(path='/home/drewp/.config/i3',       target='../own/config/i3', force=True)
+        files.link(path='/home/drewp/.emacs.d',         target='own/config/emacs-d', force=True)
+        files.link(path='/home/drewp/.fonts',           target='own/config/fonts', force=True)
+        files.link(path='/home/drewp/.fvwm2rc',         target='own/config/fvwm2rc', force=True)
+        files.link(path='/home/drewp/.hgrc',            target='own/config/hgrc', force=True)
+        files.link(path='/home/drewp/.kitty',           target='own/config/kitty', force=True)
+        files.link(path='/home/drewp/.zshrc',           target='own/config/zshrc', force=True)
+        files.link(path='/home/drewp/bin',              target='own/config/bin/', force=True)
+        files.link(path='/home/drewp/blenderkit_data',  target='own/gfx-lib/blenderkit_data/', force=True)
+
+#drwx------  3 drewp drewp  4096 Jul 31 15:07 .config/syncthing
+#npm.rc?
+# run on bang: pnpm server --background start
+
+operations = [
+    drewp_home_sync_links,
+]
\ No newline at end of file
--- a/k8s_reserve/deploy.yaml	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: reserve-cpu-dash
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app.kubernetes.io/name: reserve-cpu-dash
-  template:
-    metadata:
-      labels:
-        app.kubernetes.io/name: reserve-cpu-dash
-      annotations: { prometheus.io/scrape: "false" }
-    spec:
-      containers:
-        - name: sleep
-          image: "docker.io/rancher/pause:3.6"
-          resources: {requests: {cpu: "3"}}
-      affinity:
-        nodeAffinity:
-          requiredDuringSchedulingIgnoredDuringExecution:
-            nodeSelectorTerms:
-            - matchExpressions:
-              - key: "kubernetes.io/hostname"
-                operator: In
-                values: ["dash"]
--- a/k8s_reserve/readme	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-It's ok for k8s to use dash's ram, but it can also tie up so much cpu (notably with frigate) that I notice stalls on my desktop.
-
-The plan is to make a sleeper job with required.cpu=N to reserve N cpus from getting scheduled with real work.
--- a/kube.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,192 +0,0 @@
-import io
-import os
-import subprocess
-from tempfile import NamedTemporaryFile
-
-from pyinfra import host
-from pyinfra.facts.files import FindInFile
-from pyinfra.facts.server import Arch, LinuxDistribution
-from pyinfra.operations import files, server, systemd, apt
-
-# https://github.com/GoogleContainerTools/skaffold/releases
-skaffold_version = 'v2.13.2'
-
-
-def download_k3s(k3s_version):
-    tail = 'k3s' if host.get_fact(Arch) == 'x86_64' else 'k3s-armhf'
-    if host.get_fact(Arch) == 'aarch64':
-        tail = 'k3s-arm64'
-    files.download(
-        src=f'https://github.com/rancher/k3s/releases/download/{k3s_version}/{tail}',
-        dest='/usr/local/bin/k3s',
-        user='root',
-        group='root',
-        mode='755',
-        cache_time=43000,
-        # force=True,  # to get a new version
-    )
-
-
-def install_skaffold(reg):
-    files.download(src=f'https://storage.googleapis.com/skaffold/releases/{skaffold_version}/skaffold-linux-amd64',
-                   dest='/usr/local/bin/skaffold',
-                   user='root',
-                   group='root',
-                   mode='755',
-                   cache_time=1000)
-    # one time; writes to $HOME
-    server.shell(commands=f"skaffold config set --global insecure-registries {reg}")
-
-
-def host_prep():
-    server.sysctl(key='net.ipv4.ip_forward', value="1", persist=True)
-    server.sysctl(key='net.ipv6.conf.all.forwarding', value="1", persist=True)
-    server.sysctl(key='fs.inotify.max_user_instances', value='8192', persist=True)
-    server.sysctl(key='fs.inotify.max_user_watches', value='524288', persist=True)
-
-    # https://sysctl-explorer.net/net/ipv4/rp_filter/
-    none, strict, loose = 0, 1, 2
-    server.sysctl(key='net.ipv4.conf.default.rp_filter', value=loose, persist=True)
-
-
-# don't try to get aufs-dkms on rpi-- https://github.com/docker/for-linux/issues/709
-def podman_insecure_registry(reg):
-    # docs: https://rancher.com/docs/k3s/latest/en/installation/private-registry/
-    # user confusions: https://github.com/rancher/k3s/issues/1802
-    files.template(src='templates/kube/registries.yaml.j2', dest='/etc/rancher/k3s/registries.yaml', reg=reg)
-
-    files.template(src='templates/kube/podman_registries.conf.j2', dest='/etc/containers/registries.conf.d/reg.conf', reg=reg)
-    if host.data.get('k8s_admin'):
-        systemd.service(service='podman', user_mode=True)
-        systemd.service(service='podman.socket', user_mode=True)
-    # and maybe edit /etc/containers/policy.json
-
-
-def config_and_run_service(k3s_version, server_node, server_ip):
-    download_k3s(k3s_version)
-    service_name = 'k3s.service' if host.name == server_node else 'k3s-node.service'
-    role = 'server' if host.name == server_node else 'agent'
-    which_conf = 'config-server.yaml.j2' if host.name == server_node else 'config-agent.yaml.j2'
-
-    files.put(src="files/kube/kubelet.config", dest="/etc/rancher/k3s/kubelet.config")
-
-    # /var/lib/rancher/k3s/server/node-token is the source of the string in secrets/k3s_token,
-    # so this presumes a previous run
-    if host.name == server_node:
-        token = "ununsed"
-    else:
-        # this assumes localhost is the k3s server.
-        if not os.path.exists('/var/lib/rancher/k3s/server/node-token'):
-            print("first pass is for server only- skipping other nodes")
-            return
-        token = open('/var/lib/rancher/k3s/server/node-token', 'rt').read().strip()
-    files.template(
-        src=f'templates/kube/{which_conf}',
-        dest='/etc/k3s_config.yaml',
-        server_ip=server_ip,
-        token=token,
-        wg_ip=host.host_data['wireguard_address'],
-    )
-    files.template(
-        src='templates/kube/k3s.service.j2',
-        dest=f'/etc/systemd/system/{service_name}',
-        role=role,
-    )
-    if not host.data.get('gpu'):
-        # no supported gpu
-        '''
-            kubectl label --overwrite node bang nvidia.com/gpu.deploy.gpu-feature-discovery=false
-            kubectl label --overwrite node bang nvidia.com/gpu.deploy.container-toolkit=false
-            kubectl label --overwrite node bang nvidia.com/gpu.deploy.dcgm-exporter=false
-            kubectl label --overwrite node bang nvidia.com/gpu.deploy.device-plugin=false
-            kubectl label --overwrite node bang nvidia.com/gpu.deploy.driver=false
-            kubectl label --overwrite node bang nvidia.com/gpu.deploy.mig-manager=false
-            kubectl label --overwrite node bang nvidia.com/gpu.deploy.operator-validator=false
-        '''
-    systemd.service(service=service_name, daemon_reload=True, enabled=True, restarted=True)
-
-
-def setupNvidiaToolkit():
-    # guides:
-    #   https://github.com/NVIDIA/k8s-device-plugin#prerequisites
-    #   https://docs.k3s.io/advanced#nvidia-container-runtime-support
-    # apply this once to kube-system: https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.3/nvidia-device-plugin.yml
-    # apply this once: https://raw.githubusercontent.com/NVIDIA/gpu-feature-discovery/v0.8.2/deployments/static/nfd.yaml
-    # and: kubectl apply -f https://raw.githubusercontent.com/NVIDIA/gpu-feature-discovery/v0.8.2/deployments/static/gpu-feature-discovery-daemonset.yaml
-
-    # k3s says they do this:
-    #server.shell('nvidia-ctk runtime configure --runtime=containerd --config /var/lib/rancher/k3s/agent/etc/containerd/config.toml')
-
-    # then caller restarts k3s which includes containerd
-
-    # tried https://github.com/k3s-io/k3s/discussions/9231#discussioncomment-8114243
-    pass
-
-
-def make_cluster(
-    server_ip,
-    server_node,
-    nodes,
-    # https://github.com/k3s-io/k3s/releases
-    # 1.23.6 per https://github.com/cilium/cilium/issues/20331
-    k3s_version,
-):
-    if host.name in nodes + [server_node]:
-        host_prep()
-        files.directory(path='/etc/rancher/k3s')
-
-        podman_insecure_registry(reg='reg:5000')
-        # also note that podman dropped the default `docker.io/` prefix on image names (see https://unix.stackexchange.com/a/701785/419418)
-        config_and_run_service(k3s_version, server_node, server_ip)
-
-    if host.data.get('k8s_admin'):
-        files.directory(path='/etc/rancher/k3s')
-        install_skaffold("reg:5000")
-        files.link(path='/usr/local/bin/kubectl', target='/usr/local/bin/k3s')
-        files.directory(path='/home/drewp/.kube', user='drewp', group='drewp')
-
-        # assumes our pyinfra process is running on server_node
-        files.put(
-            src='/etc/rancher/k3s/k3s.yaml',
-            dest='/etc/rancher/k3s/k3s.yaml',  #
-            user='root',
-            group='drewp',
-            mode='640')
-        server.shell(
-            commands=f"kubectl config set-cluster default --server=https://{server_ip}:6443 --kubeconfig=/etc/rancher/k3s/k3s.yaml"
-        )
-
-
-def run_non_k8s_telegraf(node):
-    if host.name != node:
-        return
-    # this CM is written by /my/serv/telegraf/tasks.py
-    conf = io.BytesIO(subprocess.check_output(["kubectl", "get", "cm", "telegraf-config", "-o", "jsonpath={.data." + node + "}"]))
-    apt.packages(packages=['telegraf'])
-    files.put(src=conf, dest="/etc/telegraf/telegraf.conf", create_remote_dir=True, assume_exists=True)
-    systemd.service(
-        service='telegraf',
-        running=True,
-        enabled=True,
-        restarted=True,
-    )
-
-
-make_cluster(
-    server_ip="10.5.0.7",
-    server_node='ditto',
-    nodes=[
-        'bang',
-        'slash',
-        'dash',
-        'ws-printer',
-        'ga-iot',
-        'li-drums',
-        #  'gn-music',
-    ],
-    k3s_version='v1.29.1+k3s1')
-
-run_non_k8s_telegraf('pipe')
-# consider https://github.com/derailed/k9s/releases/download/v0.32.4/k9s_Linux_amd64.tar.gz
-
-# k label node ws-printer unschedulable=octoprint-allowed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/coredns/coredns.yaml	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,228 @@
+unused? needs server ip fixes
+
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: coredns
+  namespace: kube-system
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    kubernetes.io/bootstrapping: rbac-defaults
+  name: system:coredns
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - endpoints
+  - services
+  - pods
+  - namespaces
+  verbs:
+  - list
+  - watch
+- apiGroups:
+  - discovery.k8s.io
+  resources:
+  - endpointslices
+  verbs:
+  - list
+  - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  annotations:
+    rbac.authorization.kubernetes.io/autoupdate: "true"
+  labels:
+    kubernetes.io/bootstrapping: rbac-defaults
+  name: system:coredns
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: system:coredns
+subjects:
+- kind: ServiceAccount
+  name: coredns
+  namespace: kube-system
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: coredns
+  namespace: kube-system
+data:
+  Corefile: |
+    # update 2022-11-26T23:47
+    .:53 {
+        errors
+        health
+        ready
+        kubernetes cluster.local in-addr.arpa ip6.arpa {
+          pods insecure
+          fallthrough in-addr.arpa ip6.arpa
+        }
+        hosts /etc/coredns/NodeHosts {
+          ttl 60
+          reload 15s
+          fallthrough
+        }
+        prometheus :9153
+        forward . dns://10.2.0.3
+        cache 30
+        loop
+        reload
+        loadbalance
+        log
+    }
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: coredns
+  namespace: kube-system
+  labels:
+    k8s-app: kube-dns
+    kubernetes.io/name: "CoreDNS"
+spec:
+  #replicas: 1
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 1
+  selector:
+    matchLabels:
+      k8s-app: kube-dns
+  template:
+    metadata:
+      labels:
+        k8s-app: kube-dns
+    spec:
+      priorityClassName: "system-cluster-critical"
+      serviceAccountName: coredns
+      tolerations:
+        - key: "CriticalAddonsOnly"
+          operator: "Exists"
+        - key: "node-role.kubernetes.io/control-plane"
+          operator: "Exists"
+          effect: "NoSchedule"
+        - key: "node-role.kubernetes.io/master"
+          operator: "Exists"
+          effect: "NoSchedule"
+      nodeSelector:
+        kubernetes.io/os: linux
+      affinity: # because dns is broken so often, and it might be a circular config that can't start unless this is on bang
+        nodeAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            nodeSelectorTerms:
+            - matchExpressions:
+              - key: "kubernetes.io/hostname"
+                operator: In
+                values: ["bang"]
+      topologySpreadConstraints:
+        - maxSkew: 1
+          topologyKey: kubernetes.io/hostname
+          whenUnsatisfiable: DoNotSchedule
+          labelSelector:
+            matchLabels:
+              k8s-app: kube-dns
+      containers:
+      - name: coredns
+        image: rancher/mirrored-coredns-coredns:1.9.1
+        imagePullPolicy: IfNotPresent
+        resources:
+          limits:
+            memory: 170Mi
+          requests:
+            cpu: 100m
+            memory: 70Mi
+        args: [ "-conf", "/etc/coredns/Corefile" ]
+        volumeMounts:
+        - name: config-volume
+          mountPath: /etc/coredns
+          readOnly: true
+        - name: custom-config-volume
+          mountPath: /etc/coredns/custom
+          readOnly: true
+        ports:
+        - containerPort: 53
+          name: dns
+          protocol: UDP
+        - containerPort: 53
+          name: dns-tcp
+          protocol: TCP
+        - containerPort: 9153
+          name: metrics
+          protocol: TCP
+        securityContext:
+          allowPrivilegeEscalation: false
+          capabilities:
+            add:
+            - NET_BIND_SERVICE
+            drop:
+            - all
+          readOnlyRootFilesystem: true
+        livenessProbe:
+          httpGet:
+            path: /health
+            port: 8080
+            scheme: HTTP
+          initialDelaySeconds: 60
+          periodSeconds: 10
+          timeoutSeconds: 1
+          successThreshold: 1
+          failureThreshold: 3
+        readinessProbe:
+          httpGet:
+            path: /ready
+            port: 8181
+            scheme: HTTP
+          initialDelaySeconds: 0
+          periodSeconds: 2
+          timeoutSeconds: 1
+          successThreshold: 1
+          failureThreshold: 3
+      dnsPolicy: Default
+      volumes:
+        - name: config-volume
+          configMap:
+            name: coredns
+            items:
+            - key: Corefile
+              path: Corefile
+            - key: NodeHosts
+              path: NodeHosts
+        - name: custom-config-volume
+          configMap:
+            name: coredns-custom
+            optional: true
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: kube-dns
+  namespace: kube-system
+  annotations:
+    prometheus.io/port: "9153"
+    prometheus.io/scrape: "true"
+  labels:
+    k8s-app: kube-dns
+    kubernetes.io/cluster-service: "true"
+    kubernetes.io/name: "CoreDNS"
+spec:
+  selector:
+    k8s-app: kube-dns
+  clusterIP: '10.5.0.1'
+  ports:
+  - name: dns
+    port: 53
+    protocol: UDP
+  - name: dns-tcp
+    port: 53
+    protocol: TCP
+  - name: metrics
+    port: 9153
+    protocol: TCP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/coredns/coredns_freshen.sh	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,3 @@
+#!/bin/zsh
+kubectl apply -f ./coredns.yaml
+kubectl get -n kube-system configmap/coredns -o yaml 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/files/kubelet.config	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,3 @@
+apiVersion: kubelet.config.k8s.io/v1beta1
+kind: KubeletConfiguration
+maxPods: 250
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/k3s-killall.sh	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,76 @@
+#!/bin/sh
+[ $(id -u) -eq 0 ] || exec sudo $0 $@
+
+for bin in /var/lib/rancher/k3s/data/**/bin/; do
+    [ -d $bin ] && export PATH=$PATH:$bin:$bin/aux
+done
+
+set -x
+
+for service in /etc/systemd/system/k3s*.service; do
+    [ -s $service ] && systemctl stop $(basename $service)
+done
+
+for service in /etc/init.d/k3s*; do
+    [ -x $service ] && $service stop
+done
+
+pschildren() {
+    ps -e -o ppid= -o pid= | \
+    sed -e 's/^\s*//g; s/\s\s*/\t/g;' | \
+    grep -w "^$1" | \
+    cut -f2
+}
+
+pstree() {
+    for pid in $@; do
+        echo $pid
+        for child in $(pschildren $pid); do
+            pstree $child
+        done
+    done
+}
+
+killtree() {
+    kill -9 $(
+        { set +x; } 2>/dev/null;
+        pstree $@;
+        set -x;
+    ) 2>/dev/null
+}
+
+getshims() {
+    ps -e -o pid= -o args= | sed -e 's/^ *//; s/\s\s*/\t/;' | grep -w 'k3s/data/[^/]*/bin/containerd-shim' | cut -f1
+}
+
+killtree $({ set +x; } 2>/dev/null; getshims; set -x)
+
+do_unmount_and_remove() {
+    set +x
+    while read -r _ path _; do
+        case "$path" in $1*) echo "$path" ;; esac
+    done < /proc/self/mounts | sort -r | xargs -r -t -n 1 sh -c 'umount "$0" && rm -rf "$0"'
+    set -x
+}
+
+do_unmount_and_remove '/run/k3s'
+do_unmount_and_remove '/var/lib/rancher/k3s'
+do_unmount_and_remove '/var/lib/kubelet/pods'
+do_unmount_and_remove '/var/lib/kubelet/plugins'
+do_unmount_and_remove '/run/netns/cni-'
+
+# Remove CNI namespaces
+ip netns show 2>/dev/null | grep cni- | xargs -r -t -n 1 ip netns delete
+
+# Delete network interface(s) that match 'master cni0'
+ip link show 2>/dev/null | grep 'master cni0' | while read ignore iface ignore; do
+    iface=${iface%%@*}
+    [ -z "$iface" ] || ip link delete $iface
+done
+ip link delete cni0
+ip link delete flannel.1
+ip link delete flannel-v6.1
+ip link delete kube-ipvs0
+rm -rf /var/lib/cni/
+iptables-save | grep -v KUBE- | grep -v CNI- | grep -v flannel | iptables-restore
+ip6tables-save | grep -v KUBE- | grep -v CNI- | grep -v flannel | ip6tables-restore
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/k3s-uninstall.sh	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,53 @@
+#!/bin/sh
+set -x
+[ $(id -u) -eq 0 ] || exec sudo $0 $@
+
+/usr/local/bin/k3s-killall.sh
+
+if command -v systemctl; then
+    systemctl disable k3s
+    systemctl reset-failed k3s
+    systemctl daemon-reload
+fi
+if command -v rc-update; then
+    rc-update delete k3s default
+fi
+
+rm -f /etc/systemd/system/k3s.service
+rm -f /etc/systemd/system/k3s.service.env
+
+remove_uninstall() {
+    rm -f /usr/local/bin/k3s-uninstall.sh
+}
+trap remove_uninstall EXIT
+
+if (ls /etc/systemd/system/k3s*.service || ls /etc/init.d/k3s*) >/dev/null 2>&1; then
+    set +x; echo 'Additional k3s services installed, skipping uninstall of k3s'; set -x
+    exit
+fi
+
+for cmd in kubectl crictl ctr; do
+    if [ -L /usr/local/bin/$cmd ]; then
+        rm -f /usr/local/bin/$cmd
+    fi
+done
+
+rm -rf /etc/rancher/k3s
+rm -rf /run/k3s
+rm -rf /run/flannel
+rm -rf /var/lib/rancher/k3s
+rm -rf /var/lib/kubelet
+rm -f /usr/local/bin/k3s
+rm -f /usr/local/bin/k3s-killall.sh
+
+if type yum >/dev/null 2>&1; then
+    yum remove -y k3s-selinux
+    rm -f /etc/yum.repos.d/rancher-k3s-common*.repo
+elif type zypper >/dev/null 2>&1; then
+    uninstall_cmd="zypper remove -y k3s-selinux"
+    if [ "${TRANSACTIONAL_UPDATE=false}" != "true" ] && [ -x /usr/sbin/transactional-update ]; then
+        uninstall_cmd="transactional-update --no-selfupdate -d run $uninstall_cmd"
+    fi
+    $uninstall_cmd
+    rm -f /etc/zypp/repos.d/rancher-k3s-common*.repo
+fi
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/k8s_reserve/deploy.yaml	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,27 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: reserve-cpu-dash
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: reserve-cpu-dash
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: reserve-cpu-dash
+      annotations: { prometheus.io/scrape: "false" }
+    spec:
+      containers:
+        - name: sleep
+          image: "docker.io/rancher/pause:3.6"
+          resources: {requests: {cpu: "3"}}
+      affinity:
+        nodeAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            nodeSelectorTerms:
+            - matchExpressions:
+              - key: "kubernetes.io/hostname"
+                operator: In
+                values: ["dash"]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/k8s_reserve/readme	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,3 @@
+It's ok for k8s to use dash's ram, but it can also tie up so much cpu (notably with frigate) that I notice stalls on my desktop.
+
+The plan is to make a sleeper job with required.cpu=N to reserve N cpus from getting scheduled with real work.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/kube.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,203 @@
+import io
+import os
+import subprocess
+from tempfile import NamedTemporaryFile
+
+from pyinfra.context import host
+from pyinfra.facts.files import FindInFile
+from pyinfra.facts.server import Arch, LinuxDistribution
+from pyinfra.operations import files, server, systemd, apt
+
+# https://github.com/GoogleContainerTools/skaffold/releases
+skaffold_version = 'v2.13.2'
+
+
+def download_k3s(k3s_version):
+    match host.get_fact(Arch):
+        case 'x86_64':
+            tail,sha = 'k3s','dd320550cb32b053f78fb6442a1cd2c0188428c5c817482763a7fb32ea3b87b8'
+        case 'aarch64':
+            tail,sha = 'k3s-arm64','9c4cc7586c4999650edbb5312f114d4e9c6517143b1234206a32464397c77c41'
+        case _:
+            raise ValueError(f"unknown arch: {host.get_fact(Arch)}")
+    files.download(
+        src=f'https://github.com/rancher/k3s/releases/download/{k3s_version}/{tail}',
+        dest='/usr/local/bin/k3s',
+        user='root',
+        group='root',
+        mode='755',
+        sha256sum=sha,
+        cache_time=1000,
+    )
+
+
+def install_skaffold(reg):
+    files.download(src=f'https://storage.googleapis.com/skaffold/releases/{skaffold_version}/skaffold-linux-amd64',
+                   dest='/usr/local/bin/skaffold',
+                   user='root',
+                   group='root',
+                   mode='755',
+                   cache_time=1000)
+    # one time; writes to $HOME
+    server.shell(commands=f"skaffold config set --global insecure-registries {reg}")
+
+
+def host_prep():
+    server.sysctl(key='net.ipv4.ip_forward', value="1", persist=True)
+    server.sysctl(key='net.ipv6.conf.all.forwarding', value="1", persist=True)
+    server.sysctl(key='fs.inotify.max_user_instances', value='8192', persist=True)
+    server.sysctl(key='fs.inotify.max_user_watches', value='524288', persist=True)
+
+    # https://sysctl-explorer.net/net/ipv4/rp_filter/
+    none, strict, loose = 0, 1, 2
+    server.sysctl(key='net.ipv4.conf.default.rp_filter', value=loose, persist=True)
+
+
+# don't try to get aufs-dkms on rpi-- https://github.com/docker/for-linux/issues/709
+def podman_insecure_registry(reg):
+    # docs: https://rancher.com/docs/k3s/latest/en/installation/private-registry/
+    # user confusions: https://github.com/rancher/k3s/issues/1802
+    files.template(src='kube/templates/registries.yaml.j2', dest='/etc/rancher/k3s/registries.yaml', reg=reg)
+
+    files.template(src='kube/templates/podman_registries.conf.j2', dest='/etc/containers/registries.conf.d/reg.conf', reg=reg)
+    if host.data.get('k8s_admin'):
+        systemd.service(service='podman', user_mode=True)
+        systemd.service(service='podman.socket', user_mode=True)
+    # and maybe edit /etc/containers/policy.json
+
+
+def config_and_run_service(k3s_version, server_node, server_ip):
+    download_k3s(k3s_version)
+    service_name = 'k3s.service' if host.name == server_node else 'k3s-node.service'
+    role = 'server' if host.name == server_node else 'agent'
+    which_conf = 'config-server.yaml.j2' if host.name == server_node else 'config-agent.yaml.j2'
+
+    files.put(src="kube/files/kubelet.config", dest="/etc/rancher/k3s/kubelet.config")
+
+    # /var/lib/rancher/k3s/server/node-token is the source of the string in secrets/k3s_token,
+    # so this presumes a previous run
+    if host.name == server_node:
+        token = "ununsed"
+    else:
+        # this assumes localhost is the k3s server.
+        if not os.path.exists('/var/lib/rancher/k3s/server/node-token'):
+            print("first pass is for server only- skipping other nodes")
+            return
+        token = open('/var/lib/rancher/k3s/server/node-token', 'rt').read().strip()
+    files.template(
+        src=f'kube/templates/{which_conf}',
+        dest='/etc/k3s_config.yaml',
+        server_ip=server_ip,
+        token=token,
+        wg_ip=host.host_data['wireguard_address'],
+    )
+    files.template(
+        src='kube/templates/k3s.service.j2',
+        dest=f'/etc/systemd/system/{service_name}',
+        role=role,
+    )
+    if not host.data.get('gpu'):
+        # no supported gpu
+        '''
+            kubectl label --overwrite node bang nvidia.com/gpu.deploy.gpu-feature-discovery=false
+            kubectl label --overwrite node bang nvidia.com/gpu.deploy.container-toolkit=false
+            kubectl label --overwrite node bang nvidia.com/gpu.deploy.dcgm-exporter=false
+            kubectl label --overwrite node bang nvidia.com/gpu.deploy.device-plugin=false
+            kubectl label --overwrite node bang nvidia.com/gpu.deploy.driver=false
+            kubectl label --overwrite node bang nvidia.com/gpu.deploy.mig-manager=false
+            kubectl label --overwrite node bang nvidia.com/gpu.deploy.operator-validator=false
+        '''
+    systemd.service(service=service_name, daemon_reload=True, enabled=True, restarted=True)
+
+
+def setupNvidiaToolkit():
+    # guides:
+    #   https://github.com/NVIDIA/k8s-device-plugin#prerequisites
+    #   https://docs.k3s.io/advanced#nvidia-container-runtime-support
+    # apply this once to kube-system: https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.3/nvidia-device-plugin.yml
+    # apply this once: https://raw.githubusercontent.com/NVIDIA/gpu-feature-discovery/v0.8.2/deployments/static/nfd.yaml
+    # and: kubectl apply -f https://raw.githubusercontent.com/NVIDIA/gpu-feature-discovery/v0.8.2/deployments/static/gpu-feature-discovery-daemonset.yaml
+
+    # k3s says they do this:
+    #server.shell('nvidia-ctk runtime configure --runtime=containerd --config /var/lib/rancher/k3s/agent/etc/containerd/config.toml')
+
+    # then caller restarts k3s which includes containerd
+
+    # tried https://github.com/k3s-io/k3s/discussions/9231#discussioncomment-8114243
+    pass
+
+
+def make_cluster(
+    server_ip,
+    server_node,
+    nodes,
+    # https://github.com/k3s-io/k3s/releases
+    # 1.23.6 per https://github.com/cilium/cilium/issues/20331
+    k3s_version,
+):
+    if host.name in nodes + [server_node]:
+        host_prep()
+        files.directory(path='/etc/rancher/k3s')
+
+        podman_insecure_registry(reg='reg:5000')
+        # also note that podman dropped the default `docker.io/` prefix on image names (see https://unix.stackexchange.com/a/701785/419418)
+        config_and_run_service(k3s_version, server_node, server_ip)
+
+    if host.data.get('k8s_admin'):
+        files.directory(path='/etc/rancher/k3s')
+        install_skaffold("reg:5000")
+        files.link(path='/usr/local/bin/kubectl', target='/usr/local/bin/k3s')
+        files.directory(path='/home/drewp/.kube', user='drewp', group='drewp')
+
+        # assumes our pyinfra process is running on server_node
+        files.put(
+            src='/etc/rancher/k3s/k3s.yaml',
+            dest='/etc/rancher/k3s/k3s.yaml',  #
+            user='root',
+            group='drewp',
+            mode='640')
+        server.shell(
+            commands=f"kubectl config set-cluster default --server=https://{server_ip}:6443 --kubeconfig=/etc/rancher/k3s/k3s.yaml"
+        )
+
+
+def run_non_k8s_telegraf(node):
+    if host.name != node:
+        return
+    # this CM is written by /my/serv/telegraf/tasks.py
+    conf = io.BytesIO(subprocess.check_output(["kubectl", "get", "cm", "telegraf-config", "-o", "jsonpath={.data." + node + "}"]))
+    apt.packages(packages=['telegraf'])
+    files.put(src=conf, dest="/etc/telegraf/telegraf.conf", create_remote_dir=True, assume_exists=True)
+    systemd.service(
+        service='telegraf',
+        running=True,
+        enabled=True,
+        restarted=True,
+    )
+
+
+def main_cluster():
+    make_cluster(
+        server_ip="10.5.0.7",
+        server_node='ditto',
+        nodes=[
+            'bang',
+            'slash',
+            'dash',
+            'ws-printer',
+            'ga-iot',
+            'li-drums',
+            #  'gn-music',
+        ],
+        k3s_version='v1.29.1+k3s1')
+
+    run_non_k8s_telegraf('pipe')
+
+
+operations = [
+    main_cluster,
+]
+
+# consider https://github.com/derailed/k9s/releases/download/v0.32.4/k9s_Linux_amd64.tar.gz
+
+# k label node ws-printer unschedulable=octoprint-allowed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/templates/config-agent.yaml.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,3 @@
+node-ip: {{ wg_ip }}
+token: {{ token }}
+server: https://{{ server_ip }}:6443 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/templates/config-server.yaml.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,4 @@
+write-kubeconfig-mode: '640'
+node-ip: {{ wg_ip }}
+disable:
+  - traefik
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/templates/k3s.service.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,26 @@
+# written by pyinfra
+
+[Unit]
+Description=Lightweight Kubernetes
+Documentation=https://k3s.io
+After=network-online.target
+
+[Service]
+Type=notify
+ExecStartPre=-/sbin/modprobe br_netfilter
+ExecStartPre=-/sbin/modprobe overlay
+ExecStart=/usr/local/bin/k3s {{ role }} --config /etc/k3s_config.yaml --kubelet-arg=config=/etc/rancher/k3s/kubelet.config
+KillMode=process
+Delegate=yes
+# Having non-zero Limit*s causes performance problems due to accounting overhead
+# in the kernel. We recommend using cgroups to do container-local accounting.
+LimitNOFILE=1048576
+LimitNPROC=infinity
+LimitCORE=infinity
+TasksMax=infinity
+TimeoutStartSec=0
+Restart=always
+RestartSec=5s
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/templates/podman_registries.conf.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,3 @@
+[[registry]]
+location = "{{reg}}"
+insecure = true
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kube/templates/registries.yaml.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,10 @@
+# written by pyinfra
+
+
+# docs: https://rancher.com/docs/k3s/latest/en/installation/private-registry/
+
+
+mirrors:
+  "{{reg}}":
+    endpoint:
+      - "http://{{reg}}"
--- a/mail.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-from pyinfra import host
-from pyinfra.operations import apt, files, server, systemd
-
-if host.name == 'prime':
-    apt.packages(packages=['postfix', 'isync', 'opendkim', 'opendkim-tools'])
-    '''
-    per domain keygen:
-        prime(pts/4):~# mkdir /etc/opendkim/keys/chat.bigasterisk.com
-        prime(pts/4):~# opendkim-genkey -b 1024 -d chat.bigasterisk.com -D /etc/opendkim/keys/chat.bigasterisk.com -s default -v
-        opendkim-genkey: generating private key
-        opendkim-genkey: private key written to default.private
-        opendkim-genkey: extracting public key
-        opendkim-genkey: DNS TXT record written to default.txt
-        prime(pts/4):~# chown opendkim /etc/opendkim/keys/*/*
-    '''
-
-    files.template(src='templates/mail/opendkim-KeyTable.j2', dest='/etc/opendkim/KeyTable')
-    files.template(src='templates/mail/opendkim-SigningTable.j2', dest='/etc/opendkim/SigningTable')
-    files.template(src='templates/mail/opendkim-TrustedHosts.j2', dest='/etc/opendkim/TrustedHosts')
-    files.template(src='templates/mail/opendkim.conf.j2', dest='/etc/opendkim.conf')
-    files.put(src='secrets/mail/bigasterisk.com-default.private',
-              dest='/etc/opendkim/keys/bigasterisk.com/default.private',
-              mode='0600', user='opendkim')
-
-    files.template(src='templates/mail/opendkim.service.j2', dest='/usr/lib/systemd/system/opendkim.service')
-    systemd.service(service='opendkim.service', enabled=True, running=True, restarted=True, daemon_reload=True)
-
-    files.template(src='templates/mail/main.cf.j2', dest='/etc/postfix/main.cf')
-    files.template(src='templates/mail/mydestination.j2', dest='/etc/postfix/mydestination')
-    files.put(src='secrets/mail/aliases', dest='/etc/postfix/aliases')
-    files.put(src='secrets/mail/sender_access', dest='/etc/postfix/sender_access')
-    files.put(src='secrets/mail/virtual', dest='/etc/postfix/virtual')
-
-    server.shell(commands=[
-        'postmap /etc/postfix/sender_access',
-        'postmap /etc/postfix/virtual',
-        'postmap /etc/postfix/aliases',  # broken
-        'postfix reload',
-    ])
-    systemd.service(service='postfix@-.service', enabled=True, running=True)
-
-    # something to run ~drewp/mbsync/go at startup
-
-    server.shell(commands=[
-        "cd /home/drewp/mbsync; /usr/bin/mbsync-get-cert 10.5.0.1 > servercert",
-    ])
-
-    files.put(src='templates/file-count/file_count.py', dest='/opt/file_count.py')
-    files.template(src='templates/file-count/file-count.service.j2', dest='/etc/systemd/system/maildir-count.service')
-    systemd.service(service='maildir-count.service', enabled=True, running=True, daemon_reload=True)
-
-
-# other machines, route mail to bang or prime for delivery
-
-if host.name == 'bang':
-    apt.packages(packages=['postfix'])
-    files.template(src='templates/mail/main.cf.j2', dest='/etc/postfix/main.cf')
-    files.template(src='templates/mail/mydestination.j2', dest='/etc/postfix/mydestination')
-    files.put(src='secrets/mail/aliases', dest='/etc/postfix/aliases')
-    files.put(src='secrets/mail/sender_access', dest='/etc/postfix/sender_access')
-    files.put(src='secrets/mail/virtual', dest='/etc/postfix/virtual')
-
-    server.shell(commands=[
-        'postmap /etc/postfix/sender_access',
-        'postmap /etc/postfix/virtual',
-        'postmap /etc/postfix/aliases',
-        'postfix reload',
-    ])
-    systemd.service(service='postfix@-.service', enabled=True, running=True)
-
-    # server.shell(commands=[
-    #     # not working
-    #     "cd /my/serv/dovecot; runuser -u drewp -- invoke certs",
-    # ])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/dkim/opendkim-KeyTable	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,2 @@
+default._domainkey.bigasterisk.com bigasterisk.com:default:/etc/opendkim/keys/bigasterisk.com/default.private
+default._domainkey.chat.bigasterisk.com chat.bigasterisk.com:default:/etc/opendkim/keys/chat.bigasterisk.com/default.private
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/dkim/opendkim-SigningTable	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,2 @@
+*@bigasterisk.com default._domainkey.bigasterisk.com
+*@chat.bigasterisk.com default._domainkey.chat.bigasterisk.com
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/dkim/opendkim-TrustedHosts	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,4 @@
+127.0.0.1
+::1
+*.bigasterisk.com
+10.5.0.0/16
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/dkim/opendkim.conf	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,772 @@
+##
+## opendkim.conf -- configuration file for OpenDKIM filter
+##
+## Copyright (c) 2010-2015, 2018, The Trusted Domain Project.
+##   All rights reserved.
+##
+
+##
+## For settings that refer to a "dataset", see the opendkim(8) man page.
+##
+
+## DEPRECATED CONFIGURATION OPTIONS
+## 
+## The following configuration options are no longer valid.  They should be
+## removed from your existing configuration file to prevent potential issues.
+## Failure to do so may result in opendkim being unable to start.
+## 
+## Removed in 2.10.0:
+##   AddAllSignatureResults
+##   ADSPAction
+##   ADSPNoSuchDomain
+##   BogusPolicy
+##   DisableADSP
+##   LDAPSoftStart
+##   LocalADSP
+##   NoDiscardableMailTo
+##   On-PolicyError
+##   SendADSPReports
+##   UnprotectedPolicy
+
+## CONFIGURATION OPTIONS
+
+##  AllowSHA1Only { yes | no }
+##  	default "no"
+##
+##  By default, the filter will refuse to start if support for SHA256 is
+##  not available since this violates the strong recommendations of
+##  RFC6376 Section 3.3, which says:
+##
+##  "Verifiers MUST implement both rsa-sha1 and rsa-sha256.  Signers MUST
+##   implement and SHOULD sign using rsa-sha256."
+##
+##  This forces that violation to be explicitly selected by the administrator.
+
+# AllowSHA1Only		no
+
+##  AlwaysAddARHeader { yes | no }
+##  	default "no"
+##
+##  Add an "Authentication-Results:" header even to unsigned messages
+##  from domains with no "signs all" policy.  The reported DKIM result
+##  will be "none" in such cases.  Normally unsigned mail from non-strict
+##  domains does not cause the results header to be added.
+
+# AlwaysAddARHeader	no
+
+##  AuthservID string
+##  	default (local host name)
+##
+##  Defines the "authserv-id" token to be used when generating 
+##  Authentication-Results headers after message verification.
+
+# AuthservID		example.com
+
+##  AuthservIDWithJobID
+##  	default "no"
+##
+##  Appends a "/" followed by the MTA's job ID to the "authserv-id" token
+##  when generating Authentication-Results headers after message verification.
+
+# AuthservIDWithJobId	no
+
+##  AutoRestart { yes | no }
+##  	default "no"
+##
+##  Indicate whether or not the filter should arrange to restart automatically
+##  if it crashes.
+
+# AutoRestart		No
+
+##  AutoRestartCount n
+##  	default 0
+##
+##  Sets the maximum automatic restart count.  After this number of
+##  automatic restarts, the filter will give up and terminate.  A value of 0
+##  implies no limit.
+
+# AutoRestartCount	0
+
+##  AutoRestartRate n/t[u]
+##  	default (none)
+## 
+##  Sets the maximum automatic restart rate.  See the opendkim.conf(5)
+##  man page for the format of this parameter.
+
+# AutoRestartRate	n/tu
+
+##  Background { yes | no }
+##  	default "yes"
+##
+##  Indicate whether or not the filter should run in the background.
+
+# Background		Yes
+
+##  BaseDirectory path
+##  	default (none)
+##
+##  Causes the filter to change to the named directory before beginning
+##  operation.  Thus, cores will be dumped here and configuration files
+##  are read relative to this location.
+
+# BaseDirectory		/var/run/opendkim
+
+##  BodyLengthDB dataset
+##  	default (none)
+##
+##  A data set that is checked against envelope recipients to see if a
+##  body length tag should be included in the generated signature.
+##  This has security implications; see opendkim.conf(5) for details.
+
+# BodyLengthDB		dataset
+
+##  Canonicalization hdrcanon[/bodycanon]
+##  	default "simple/simple"
+##
+##  Select canonicalizations to use when signing.  If the "bodycanon" is
+##  omitted, "simple" is used.  Valid values for each are "simple" and
+##  "relaxed".
+
+# Canonicalization	simple/simple
+
+##  ClockDrift n
+##  	default 300
+##
+##  Specify the tolerance range for expired signatures or signatures
+##  which appear to have timestamps in the future, allowing for clock
+##  drift.
+
+# ClockDrift		300 
+
+##  Diagnostics { yes | no }
+##  	default "no"
+##
+##  Specifies whether or not signatures with header diagnostic tags should
+##  be generated.
+
+# Diagnostics		No
+
+##  DNSTimeout n
+##  	default 10
+##
+##  Specify the time in seconds to wait for replies from the nameserver when
+##  requesting keys or signing policies.
+
+# DNSTimeout		10
+
+##  Domain dataset
+##  	default (none)
+##
+##  Specify for which domain(s) signing should be done.  No default; must
+##  be specified for signing.
+
+Domain			bigasterisk.com,chat.bigasterisk.com
+
+##  DomainKeysCompat { yes | no }
+##  	default "no"
+##
+##  When enabled, backward compatibility with DomainKeys (RFC4870) key
+##  records is enabled.  Otherwise, such key records are considered to be
+##  syntactically invalid.
+
+# DomainKeysCompat	no
+
+##  DontSignMailTo	dataset
+##  	default (none)
+##
+##  Gives a list of recipient addresses or address patterns whose mail should
+##  not be signed.
+
+# DontSignMailTo	addr1,addr2,...
+
+##  EnableCoredumps { yes | no }
+##  	default "no"
+##
+##  On systems which have support for such, requests that the kernel dump
+##  core even though the process may change user ID during its execution.
+
+# EnableCoredumps	no
+
+##  ExemptDomains dataset
+##  	default (none)
+##
+##  A data set of domain names that are checked against the message sender's
+##  domain.  If a match is found, the message is ignored by the filter.
+
+# ExemptDomains		domain1,domain2,...
+
+##  ExternalIgnoreList filename
+##
+##  Names a file from which a list of externally-trusted hosts is read.
+##  These are hosts which are allowed to send mail through you for signing.
+##  Automatically contains 127.0.0.1.  See man page for file format.
+
+ExternalIgnoreList	refile:/etc/opendkim/TrustedHosts
+
+##  FixCRLF { yes | no }
+##
+##  Requests that the library convert "naked" CR and LF characters to
+##  CRLFs during canonicalization.  The default is "no".
+
+# FixCRLF 		no
+
+##  IgnoreMalformedMail { yes | no }
+##  	default "no"
+##
+##  Silently passes malformed messages without alteration.  This includes 
+##  messages that fail the RequiredHeaders check, if enabled.  The default is
+##  to pass those messages but add an Authentication-Results field indicating
+##  that they were malformed.
+
+# IgnoreMalformedMail	no
+
+##  InternalHosts dataset
+##  	default "127.0.0.1"
+##
+##  Names a file from which a list of internal hosts is read.  These are
+##  hosts from which mail should be signed rather than verified.
+##  Automatically contains 127.0.0.1.
+
+InternalHosts		refile:/etc/opendkim/TrustedHosts
+
+##  KeepTemporaryFiles { yes | no }
+##  	default "no"
+##
+##  If set, causes temporary files generated during message signing or
+##  verifying to be left behind for debugging use.  Not for normal operation;
+##  can fill your disks quite fast on busy systems.
+
+# KeepTemporaryFiles	no
+
+##  KeyFile filename
+##  	default (none)
+##
+##  Specifies the path to the private key to use when signing.  Ignored if
+##  SigningTable and KeyTable are used.  No default; must be specified for 
+##  signing if SigningTable/KeyTable are not in use.
+
+KeyFile			/etc/opendkim/keys/default.private
+
+##  KeyTable dataset
+##  	default (none)
+##
+##  Defines a table that will be queried to convert key names to
+##  sets of data of the form (signing domain, signing selector, private key).
+##  The private key can either contain a PEM-formatted private key,
+##  a base64-encoded DER format private key, or a path to a file containing
+##  one of those.
+
+KeyTable		/etc/opendkim/KeyTable
+
+##  LogWhy { yes | no }
+##  	default "no"
+##
+##  If logging is enabled (see Syslog below), issues very detailed logging
+##  about the logic behind the filter's decision to either sign a message
+##  or verify it.  The logic behind the decision is non-trivial and can be
+##  confusing to administrators not familiar with its operation.  A
+##  description of how the decision is made can be found in the OPERATIONS
+##  section of the opendkim(8) man page.  This causes a large increase
+##  in the amount of log data generated for each message, so it should be
+##  limited to debugging use and not enabled for general operation.
+
+LogWhy		yes
+
+##  MacroList macro[=value][,...]
+##
+##  Gives a set of MTA-provided macros which should be checked to see
+##  if the sender has been determined to be a local user and therefore
+##  whether or not signing should be done.  See opendkim.conf(5) for
+##  more information.
+
+# MacroList		foo=bar,baz=blivit
+
+##  MaximumHeaders n
+##
+##  Disallow messages whose header blocks are bigger than "n" bytes.
+##  Intended to detect and block a denial-of-service attack.  The default
+##  is 65536.  A value of 0 disables this test.
+
+# MaximumHeaders	n
+
+##  MaximumSignaturesToVerify n
+##  	(default 3)
+##
+##  Verify no more than "n" signatures on an arriving message.
+##  A value of 0 means "no limit".
+
+# MaximumSignaturesToVerify	n
+
+##  MaximumSignedBytes n
+##
+##  Don't sign more than "n" bytes of the message.  The default is to 
+##  sign the entire message.  Setting this implies "BodyLengths".
+
+# MaximumSignedBytes	n
+
+##  MilterDebug n
+##
+##  Request a debug level of "n" from the milter library.  The default is 0.
+
+# MilterDebug		0
+
+##  Minimum n[% | +]
+##  	default 0
+##
+##  Sets a minimum signing volume; one of the following formats:
+##	n	at least n bytes (or the whole message, whichever is less)
+##		must be signed
+##  	n%	at least n% of the message must be signed
+##	n+	if a length limit was presented in the signature, no more than
+##  		n bytes may have been added
+
+# Minimum		n
+
+##  MinimumKeyBits n
+##  	default 1024
+##
+##  Causes the library not to accept signatures matching keys made of fewer
+##  than the specified number of bits, even if they would otherwise pass
+##  DKIM signing.
+
+# MinimumKeyBits	1024
+
+##  Mode [sv]
+##  	default sv
+##
+##  Indicates which mode(s) of operation should be provided.  "s" means
+##  "sign", "v" means "verify".
+
+Mode			sv
+
+##  MTA dataset
+##  	default (none)
+##  
+##  Specifies a list of MTAs whos mail should always be signed rather than
+##  verified.  The "mtaname" is extracted from the DaemonPortOptions line
+##  in effect.
+
+# MTA			name
+
+##  MultipleSignatures { yes | no }
+##  	default no
+##
+##  Allows multiple signatures to be added.  If set to "true" and a SigningTable
+##  is in use, all SigningTable entries that match the candidate message will
+##  cause a signature to be added.  Otherwise, only the first matching
+##  SigningTable entry will be added, or only the key defined by Domain,
+##  Selector and KeyFile will be added.
+
+# MultipleSignatures	no
+
+##  MustBeSigned dataset
+##  	default (none)
+##
+##  Defines a list of headers which, if present on a message, must be
+##  signed for the signature to be considered acceptable.
+
+# MustBeSigned		header1,header2,...
+
+##  Nameservers addr1[,addr2[,...]]
+##  	default (none)
+##
+##  Provides a comma-separated list of IP addresses that are to be used when
+##  doing DNS queries to retrieve DKIM keys, VBR records, etc.
+##  These override any local defaults built in to the resolver in use, which
+##  may be defined in /etc/resolv.conf or hard-coded into the software.
+
+# Nameservers addr1,addr2,...
+
+##  NoHeaderB { yes | no }
+##  	default "no"
+##
+##  Suppresses addition of "header.b" tags on Authentication-Results
+##  header fields.
+
+# NoHeaderB		no
+
+##  OmitHeaders dataset
+##  	default (none)
+##
+##  Specifies a list of headers that should always be omitted when signing.
+##  Header names should be separated by commas.
+
+# OmitHeaders		header1,header2,...
+
+##  On-...
+##
+##  Specifies what to do when certain error conditions are encountered.
+##
+##  See opendkim.conf(5) for more information.
+
+# On-Default
+# On-BadSignature
+# On-DNSError
+# On-InternalError
+# On-NoSignature
+# On-Security
+# On-SignatureError
+
+##  OversignHeaders dataset
+##  	default (none)
+##
+##  Specifies a set of header fields that should be included in all signature
+##  header lists (the "h=" tag) once more than the number of times they were
+##  actually present in the signed message.  See opendkim.conf(5) for more
+##  information.
+
+# OverSignHeaders	header1,header2,...
+
+##  PeerList dataset
+##  	default (none)
+##
+##  Contains a list of IP addresses, CIDR blocks, hostnames or domain names
+##  whose mail should be neither signed nor verified by this filter.  See man
+##  page for file format.
+
+# PeerList		filename
+
+##  PidFile filename
+##  	default (none)
+## 
+##  Name of the file where the filter should write its pid before beginning
+##  normal operations.
+
+# PidFile		filename
+
+##  POPDBFile dataset
+##  	default (none)
+##
+##  Names a database which should be checked for "POP before SMTP" records
+##  as a form of authentication of users who may be sending mail through
+##  the MTA for signing.  Requires special compilation of the filter.
+##  See opendkim.conf(5) for more information.
+
+# POPDBFile		filename
+
+##  Quarantine { yes | no }
+##  	default "no"
+##
+##  Indicates whether or not the filter should arrange to quarantine mail
+##  which fails verification.  Intended for diagnostic use only.
+
+# Quarantine		No
+
+##  QueryCache { yes | no }
+##  	default "no"
+##
+##  Instructs the DKIM library to maintain its own local cache of keys and
+##  policies retrieved from DNS, rather than relying on the nameserver for
+##  caching service.  Useful if the nameserver being used by the filter is
+##  not local.  The filter must be compiled with the QUERY_CACHE flag to enable
+##  this feature, since it adds a library dependency.
+
+# QueryCache		No
+
+##  RedirectFailuresTo address
+##  	default (none)
+##
+##  Redirects signed messages to the specified address if none of the
+##  signatures present failed to verify.
+
+# RedirectFailuresTo	postmaster@example.com
+
+##  RemoveARAll { yes | no }
+##  	default "no"
+##
+##  Remove all Authentication-Results: headers on all arriving mail.
+
+# RemoveARAll		No
+
+##  RemoveARFrom dataset
+##  	default (none)
+##
+##  Remove all Authentication-Results: headers on all arriving mail that
+##  claim to have been added by hosts listed in this parameter.  The list
+##  should be comma-separated.  Entire domains may be specified by preceding
+##  the dopmain name by a single dot (".") character.
+
+# RemoveARFrom		host1,host2,.domain1,.domain2,...
+
+##  RemoveOldSignatures { yes | no }
+##  	default "no"
+##
+##  Remove old signatures on messages, if any, when generating a signature.
+
+# RemoveOldSignatures	No
+
+##  ReportAddress addr
+##  	default (executing user)@(hostname)
+##
+##  Specifies the sending address to be used on From: headers of outgoing
+##  failure reports.  By default, the e-mail address of the user executing
+##  the filter is used.
+
+# ReportAddress		"DKIM Error Postmaster" <postmaster@example.com>
+
+##  ReportBccAddress addr
+##  	default (none)
+##
+##  Specifies additional recipient address(es) to receive outgoing failure
+##  reports.
+
+# ReportBccAddress	postmaster@example.com, john@example.com
+
+##  RequiredHeaders { yes | no }
+##  	default no
+##
+##  Rejects messages which don't conform to RFC5322 header count requirements.
+
+# RequiredHeaders	No
+
+##  RequireSafeKeys { yes | no }
+##  	default yes
+##
+##  Refuses to use key files that appear to have unsafe permissions.
+
+# RequireSafeKeys	Yes
+RequireSafeKeys false
+
+##  ResignAll { yes | no }
+##  	default no
+##
+##  Where ResignMailTo triggers a re-signing action, this flag indicates
+##  whether or not all mail should be signed (if set) versus only verified
+##  mail being signed (if not set).
+
+# ResignAll		No
+
+##  ResignMailTo dataset
+##  	default (none)
+##
+##  Checks each message recipient against the specified dataset for a
+##  matching record.  The full address is checked in each case, then the
+##  hostname, then each domain preceded by ".".  If there is a match, the
+##  value returned is presumed to be the name of a key in the KeyTable
+##  (if defined) to be used to re-sign the message in addition to
+##  verifying it.  If there is a match without a KeyTable, the default key
+##  is applied.
+
+# ResignMailTo		dataset
+
+##  ResolverConfiguration string
+##
+##  Passes arbitrary configuration data to the resolver.  For the stock UNIX
+##  resolver, this is ignored; for Unbound, it names a resolv.conf(5)-style
+##  file that should be read for configuration information.
+
+# ResolverConfiguration	string
+
+##  ResolverTracing { yes | no }
+##
+##  Requests enabling of resolver trace features, if available.  The effect
+##  of setting this flag depends on how trace features, if any, are implemented
+##  in the resolver in use.  Currently only effective when used with the
+##  OpenDKIM asynchronous resolver.
+
+# ResolverTracing	no
+
+##  Selector name
+##
+##  The name of the selector to use when signing.  No default; must be
+##  specified for signing.
+
+Selector		default
+
+##  SenderHeaders 	dataset
+##  	default (none)
+##
+##  Overrides the default list of headers that will be used to determine
+##  the sending domain when deciding whether to sign the message and with
+##  with which key(s).  See opendkim.conf(5) for details.
+
+# SenderHeaders		From
+
+##  SendReports { yes | no }
+##  	default "no"
+##
+##  Specifies whether or not the filter should generate report mail back
+##  to senders when verification fails and an address for such a purpose
+##  is provided.  See opendkim.conf(5) for details.
+
+# SendReports		No
+
+##  SignatureAlgorithm signalg
+##  	default "rsa-sha256"
+##
+##  Signature algorithm to use when generating signatures.  Must be one of
+##  "rsa-sha1", "rsa-sha256", or "ed25519-sha256".
+
+# SignatureAlgorithm	rsa-sha256
+
+##  SignatureTTL seconds
+##  	default "0"
+##
+##  Specifies the lifetime in seconds of signatures generated by the
+##  filter.  A value of 0 means no expiration time is included in the
+##  signature.
+
+# SignatureTTL		0
+
+##  SignHeaders dataset
+##  	default (none)
+##
+##  Specifies the list of headers which should be included when generating
+##  signatures.  The string should be a comma-separated list of header names.
+##  See the opendkim.conf(5) man page for more information.
+
+# SignHeaders		header1,header2,...
+
+##  SigningTable dataset
+##  	default (none)
+##
+##  Defines a dataset that will be queried for the message sender's address
+##  to determine which private key(s) (if any) should be used to sign the
+##  message.  The sender is determined from the value of the sender
+##  header fields as described with SenderHeaders above.  The key for this
+##  lookup should be an address or address pattern that matches senders;
+##  see the opendkim.conf(5) man page for more information.  The value
+##  of the lookup should return the name of a key found in the KeyTable
+##  that should be used to sign the message.  If MultipleSignatures
+##  is set, all possible lookup keys will be attempted which may result
+##  in multiple signatures being applied.
+
+SigningTable		refile:/etc/opendkim/SigningTable
+
+##  SingleAuthResult { yes | no}
+##  	default "no"
+##
+##  When DomainKeys verification is enabled, multiple Authentication-Results
+##  will be added, one for DK and one for DKIM.  With this enabled, only
+##  a DKIM result will be reported unless DKIM failed but DK passed, in which
+##  case only a DK result will be reported.
+
+# SingleAuthResult	no
+
+##  SMTPURI uri
+##
+##  Specifies a URI (e.g., "smtp://localhost") to which mail should be sent
+##  via SMTP when notifications are generated.
+
+# Socket smtp://localhost
+
+##  Socket socketspec
+##
+##  Names the socket where this filter should listen for milter connections
+##  from the MTA.  Required.  Should be in one of these forms:
+##
+##  inet:port@address		to listen on a specific interface
+##  inet:port			to listen on all interfaces
+##  local:/path/to/socket	to listen on a UNIX domain socket
+
+Socket			inet:8891@localhost
+
+##  SoftwareHeader { yes | no }
+##  	default "no"
+##
+##  Add a DKIM-Filter header field to messages passing through this filter
+##  to identify messages it has processed.
+
+# SoftwareHeader	no
+
+##  StrictHeaders { yes | no }
+##  	default "no"
+##
+##  Requests that the DKIM library refuse to process a message whose
+##  header fields do not conform to the standards, in particular Section 3.6
+##  of RFC5322.
+
+# StrictHeaders		no
+
+##  StrictTestMode { yes | no }
+##  	default "no"
+##
+##  Selects strict CRLF mode during testing (see the "-t" command line
+##  flag in the opendkim(8) man page).  Messages for which all header
+##  fields and body lines are not CRLF-terminated are considered malformed
+##  and will produce an error.
+
+# StrictTestMode	no
+
+##  SubDomains { yes | no }
+##  	default "no"
+##
+##  Sign for subdomains as well?
+
+# SubDomains		No
+
+##  Syslog { yes | no }
+##  	default "yes"
+##
+##  Log informational and error activity to syslog?
+
+Syslog			Yes
+
+##  SyslogFacility      facility
+##  	default "mail"
+##
+##  Valid values are :
+##      auth cron daemon kern lpr mail news security syslog user uucp 
+##      local0 local1 local2 local3 local4 local5 local6 local7
+##
+##  syslog facility to be used
+
+# SyslogFacility	mail
+
+##  SyslogName          ident
+##      default "opendkim" (or the name of the executable)
+##
+##  Identifier to be prepended to all generated log entries.
+
+# SyslogName		opendkim
+
+##  SyslogSuccess { yes | no }
+##  	default "no"
+##
+##  Log success activity to syslog?
+
+# SyslogSuccess		No
+
+##  TemporaryDirectory path
+##  	default /tmp
+##
+##  Specifies which directory will be used for creating temporary files
+##  during message processing.
+
+# TemporaryDirectory	/tmp
+
+##  TestPublicKeys filename
+##  	default (none)
+##
+##  Names a file from which public keys should be read.  Intended for use
+##  only during automated testing.
+
+# TestPublicKeys	/tmp/testkeys
+
+##  TrustAnchorFile filename
+##  	default (none)
+##
+## Specifies a file from which trust anchor data should be read when doing
+## DNS queries and applying the DNSSEC protocol.  See the Unbound documentation
+## at http://unbound.net for the expected format of this file.
+
+# TrustAnchorFile	/var/named/trustanchor
+
+##  UMask mask
+##  	default (none)
+##
+##  Change the process umask for file creation to the specified value.
+##  The system has its own default which will be used (usually 022).
+##  See the umask(2) man page for more information.
+
+# UMask			022
+
+# UnboundConfigFile	/var/named/unbound.conf
+
+##  Userid userid
+##  	default (none)
+##
+##  Change to user "userid" before starting normal operation?  May include
+##  a group ID as well, separated from the userid by a colon.
+
+UserID		opendkim
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/file-count/file-count.service	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,15 @@
+# written by pyinfra
+
+[Unit]
+After=wg-quick@wg0.service
+
+[Service]
+Type=exec
+KillMode=process
+# port 2500 is used in victoriametrics/config/scrape_main.yaml
+ExecStart=runuser -u drewp /usr/bin/python3 /opt/file_count.py 10.5.0.2 2500 /home/drewp/Maildir/new maildir_count
+Restart=always
+RestartSec=5s
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/file-count/file_count.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,20 @@
+import http.server
+import socketserver
+import os
+import sys
+
+interface, port, dir, metric_name = sys.argv[1:]
+
+
+class Web(http.server.SimpleHTTPRequestHandler):
+
+    def do_GET(self):
+        files_count = len(os.listdir(dir))
+        self.send_response(200)
+        self.send_header('Content-type', 'text/plain')
+        self.end_headers()
+        self.wfile.write(f'{metric_name} {files_count}'.encode())
+
+
+with socketserver.TCPServer((interface, int(port)), Web) as httpd:
+    httpd.serve_forever()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/mail.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,99 @@
+from pyinfra.context import host
+from pyinfra.operations import apt, files, server, systemd
+
+# ditto (and others?) might also run postfix; not sure how
+
+
+def dkim():
+    if host.name != 'prime':
+        return
+    '''
+    per domain keygen:
+        prime(pts/4):~# mkdir /etc/opendkim/keys/chat.bigasterisk.com
+        prime(pts/4):~# opendkim-genkey -b 1024 -d chat.bigasterisk.com -D /etc/opendkim/keys/chat.bigasterisk.com -s default -v
+        opendkim-genkey: generating private key
+        opendkim-genkey: private key written to default.private
+        opendkim-genkey: extracting public key
+        opendkim-genkey: DNS TXT record written to default.txt
+        prime(pts/4):~# chown opendkim /etc/opendkim/keys/*/*
+    '''
+    apt.packages(packages=['opendkim', 'opendkim-tools'])
+
+    files.template(src='mail/dkim/opendkim-KeyTable', dest='/etc/opendkim/KeyTable')
+    files.template(src='mail/dkim/opendkim-SigningTable', dest='/etc/opendkim/SigningTable')
+    files.template(src='mail/dkim/opendkim-TrustedHosts', dest='/etc/opendkim/TrustedHosts')
+    files.template(src='mail/dkim/opendkim.conf', dest='/etc/opendkim.conf')
+
+    for domain in ['bigasterisk.com', 'chat.bigasterisk.com']:
+        files.put(src=f'secrets/mail/{domain}-default.private',
+                  dest=f'/etc/opendkim/keys/{domain}/default.private',
+                  mode='0600',
+                  user='opendkim')
+
+    files.template(src='mail/opendkim.service', dest='/usr/lib/systemd/system/opendkim.service')
+    systemd.service(service='opendkim.service', enabled=True, running=True, restarted=True, daemon_reload=True)
+
+
+def postfix():
+    if host.name != 'prime':
+        return
+    apt.packages(packages=['postfix', 'isync'])
+
+    files.template(src='mail/main.cf.j2', dest='/etc/postfix/main.cf')
+    files.put(src='mail/mydestination', dest='/etc/postfix/mydestination')
+    files.put(src='secrets/mail/aliases', dest='/etc/postfix/aliases')
+    files.put(src='secrets/mail/sender_access', dest='/etc/postfix/sender_access')
+    files.put(src='secrets/mail/virtual', dest='/etc/postfix/virtual')
+
+    server.shell(commands=[
+        'postmap /etc/postfix/sender_access',
+        'postmap /etc/postfix/virtual',
+        'postmap /etc/postfix/aliases',  # broken
+        'postfix reload',
+    ])
+    systemd.service(service='postfix@-.service', enabled=True, running=True)
+
+
+def mbsync():
+    if host.name != 'prime':
+        return
+
+    # todo: something to run ~drewp/mbsync/go at startup
+
+    server.shell(commands=[
+        "cd /home/drewp/mbsync; /usr/bin/mbsync-get-cert 10.5.0.1 > servercert",
+    ])
+
+    files.put(src='mail/file-count/file_count.py', dest='/opt/file_count.py')
+    files.put(src='mail/file-count/file-count.service', dest='/etc/systemd/system/maildir-count.service')
+    systemd.service(service='maildir-count.service', enabled=True, running=True, daemon_reload=True)
+
+
+# other machines, route mail to bang or prime for delivery
+
+# if host.name == 'bang':
+#     apt.packages(packages=['postfix'])
+#     files.template(src='templates/mail/main.cf.j2', dest='/etc/postfix/main.cf')
+#     files.template(src='templates/mail/mydestination.j2', dest='/etc/postfix/mydestination')
+#     files.put(src='secrets/mail/aliases', dest='/etc/postfix/aliases')
+#     files.put(src='secrets/mail/sender_access', dest='/etc/postfix/sender_access')
+#     files.put(src='secrets/mail/virtual', dest='/etc/postfix/virtual')
+
+#     server.shell(commands=[
+#         'postmap /etc/postfix/sender_access',
+#         'postmap /etc/postfix/virtual',
+#         'postmap /etc/postfix/aliases',
+#         'postfix reload',
+#     ])
+#     systemd.service(service='postfix@-.service', enabled=True, running=True)
+
+#     # server.shell(commands=[
+#     #     # not working
+#     #     "cd /my/serv/dovecot; runuser -u drewp -- invoke certs",
+#     # ])
+
+operations = [
+    dkim,
+    postfix,
+    mbsync,
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/main.cf.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,104 @@
+# written by pyinfra
+
+compatibility_level = 3
+
+smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
+
+readme_directory = /usr/share/doc/postfix
+html_directory = /usr/share/doc/postfix/html
+
+inet_interfaces = all
+
+# TLS parameters
+smtpd_tls_cert_file=/etc/ssl/certs/self1-ca.crt
+smtpd_tls_key_file=/etc/ssl/certs/self1-ca.key
+smtpd_use_tls=yes
+smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
+smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
+smtpd_tls_loglevel = 0
+smtpd_tls_security_level = may
+smtpd_tls_received_header = yes
+smtpd_relay_before_recipient_restrictions = yes
+smtp_address_preference = ipv4
+
+# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
+# information on enabling SSL in the smtp client.
+
+relayhost = {{ 'prime.bigasterisk.com' if host.name != 'prime' else '' }}
+
+alias_maps = hash:/etc/postfix/aliases
+alias_database = hash:/etc/postfix/aliases
+
+{% if host.name == 'prime' %}
+myhostname = bigasterisk.com
+mydestination = /etc/postfix/mydestination
+{% else %}
+myhostname = {{ host.name }}.bigasterisk.com
+# must relay, even if you think you're the destination name is correct
+mydestination = 
+{% endif %}
+
+relay_domains = $mydestination
+mynetworks_style = subnet
+mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 10.1.0.0/16 10.3.0.0/16 10.5.0.0/24 192.168.0.3/32 [fc7b:54e8:69a9:e165:86c8:9d42:6cc5:b2a1]/128 [fcc8:29d:5660:ec63:754f:37af:de4a:a9df]/128
+
+# allow realuser+fakepart@bigasterisk.com
+recipient_delimiter = +
+
+{% if host.name == 'prime' %}
+# mail can only deliver on prime
+mailbox_size_limit = 0
+home_mailbox = Maildir/
+biff = no
+message_size_limit = 50000000
+#mailbox_command = procmail -a "$EXTENSION"
+{% endif %}
+
+
+# http://www.spamcop.net/fom-serve/cache/349.html
+# upgraded, per http://www.wrightthisway.com/Articles/000062.html
+
+smtpd_recipient_restrictions =
+    permit_mynetworks, 
+    permit_sasl_authenticated,
+#    check_client_access  /etc/passwd somehow?
+    reject_invalid_hostname, 
+    reject_non_fqdn_sender, 
+    reject_non_fqdn_recipient, 
+    reject_unknown_sender_domain, 
+    reject_unknown_recipient_domain, 
+    reject_unauth_pipelining, 
+    permit_tls_clientcerts,
+    reject_unauth_destination, 
+    check_sender_access hash:/etc/postfix/sender_access,
+    reject_rbl_client bl.spamcop.net,
+    permit
+    
+smtpd_tls_ask_ccert = yes
+
+# no dovecot
+smtpd_sasl_type = cyrus
+cyrus_sasl_config_path = /etc/postfix/sasl/
+
+# yes dovecot
+#smtpd_sasl_type = dovecot
+#smtpd_sasl_path = private/auth
+
+smtpd_sasl_auth_enable = yes
+smtpd_sasl_security_options = noanonymous
+smtpd_sasl_tls_security_options = $smtpd_sasl_security_options
+smtpd_tls_auth_only = yes
+
+queue_directory = /var/spool/postfix
+
+# Postfix is the final destination for the specified list
+{% if host.name == 'prime' %}
+virtual_alias_domains = adkinslawgroup.com iveseenyoubefore.com fantasyfamegame.com maxradi.us whopickedthis.com quickwitretort.com drewp.quickwitretort.com kelsi.quickwitretort.com photo.bigasterisk.com whatsplayingnext.com williamperttula.com 
+
+# Optional lookup tables that alias specific mail addresses or domains to other local or remote addresses
+virtual_alias_maps = hash:/etc/postfix/virtual
+{% endif %}
+
+smtpd_milters = inet:127.0.0.1:8891
+non_smtpd_milters = $smtpd_milters
+milter_default_action = accept
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/mydestination	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,20 @@
+localhost
+localhost.bigasterisk.com
+10.2.0.1
+a.mx.bigasterisk.com
+bang.bigasterisk.com
+bigast.com
+bigasterisk.com
+dash.bigasterisk.com
+mail.bigasterisk.com
+www.bigasterisk.com
+chitty.bigasterisk.com
+cuisine.bigasterisk.com
+dot.bigasterisk.com
+drewp.quickwitretort.com
+kelsi.quickwitretort.com
+maxradi.us
+williamperttula.com
+ditto.bigasterisk.com
+chat.bigasterisk.com
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mail/opendkim.service	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,15 @@
+[Unit]
+Description=OpenDKIM Milter
+Documentation=man:opendkim(8) man:opendkim.conf(5) man:opendkim-lua(3) man:opendkim-genkey(8) man:opendkim-genzone(8) man:opendkim-testkey(8) http://www.opendkim.org/docs.html
+After=network-online.target nss-lookup.target
+Wants=network-online.target
+
+[Service]
+Type=forking
+#PIDFile=/run/opendkim/opendkim.pid
+ExecStart=/usr/sbin/opendkim -vv
+#ExecReload=/bin/kill -USR1 $MAINPID
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
--- a/multikube.py	Mon Jan 20 14:10:19 2025 -0800
+++ b/multikube.py	Mon Jan 20 21:55:08 2025 -0800
@@ -1,5 +1,5 @@
 # leave kube.py running single-host and try again
-from pyinfra import host
+from pyinfra.context import host
 from pyinfra.facts.server import Arch
 from pyinfra.operations import files, server, systemd
 
--- a/net.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-from pyinfra import host
-from pyinfra.operations import apt, files, server, systemd
-
-
-def cleanup():
-    # past attempts
-    files.file(path='/etc/network/interfaces', present=False)
-
-    for search_dir in [
-            # search path per `man systemd.network`:
-            # /lib/systemd/network              # These OS files are ok.
-            '/usr/local/lib/systemd/network/',  # Probably no such dir.
-            '/run/systemd/network/',  # Previous netplan attempts dumped in here.
-            # '/etc/systemd/network/',  # I'm going to work in here.
-    ]:
-        files.sync(
-            src="files/empty_dir/",
-            dest=search_dir,
-            delete=True,
-        )
-
-    # On pipe:
-    #   Now using a HW router for this firewall. No incoming connections.
-    #   test connections from the outside:
-    #   http://www.t1shopper.com/tools/port-scanner/
-    # On prime:
-    #   using digitalocean network config:
-    #   https://cloud.digitalocean.com/networking/firewalls/f68899ae-1aac-4469-b379-59ce2bbc988f/droplets?i=7c5072
-    apt.packages(packages=['ufw'], present=False)
-
-
-def iptables_version():
-    # https://github.com/k3s-io/k3s/issues/1812 unclear, but more importantly, this has to be set
-    # on pipe in a way that works with the commands in house_net.service (and net_routes)
-    server.shell(commands=[
-        'update-alternatives --set iptables /usr/sbin/iptables-legacy',
-        'update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy',
-    ])
-    # needs reboot if this changed
-
-
-iptables_version()
-server.sysctl(key='net.ipv6.conf.all.disable_ipv6', value=1, persist=True)
-
-if host.name == 'prime':
-    cleanup()
-
-    files.template(
-        src="files/net/prime.network",
-        dest="/etc/systemd/network/99-prime.network",
-    )
-    systemd.service(service='systemd-networkd.service', enabled=True, running=True, restarted=True)
-
-if host.name == 'pipe':
-    cleanup()
-
-    files.template(src="files/net/pipe_10.2.network", dest="/etc/systemd/network/99-10.2.network")
-    files.template(src="files/net/pipe_isp.network", dest="/etc/systemd/network/99-isp.network")
-    server.sysctl(key='net.ipv4.ip_forward', value=1, persist=True)
-    files.template(src="files/net/house_net.service", dest="/etc/systemd/system/house_net.service", out_interface='eth0')
-    systemd.service(service='house_net.service', daemon_reload=True, enabled=True, running=True, restarted=True)
-    systemd.service(service='systemd-networkd.service', enabled=True, running=True, restarted=True)
-
-if host.name == 'ditto':
-    files.template(
-        src="files/net/ditto-netplan.yaml",
-        dest="/etc/netplan/00-installer-config.yaml",
-        create_remote_dir=True,
-    )
-
-    systemd.service(service='systemd-networkd.service', enabled=True, running=True, restarted=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/files/ditto-netplan.yaml	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,22 @@
+# written by pyinfra
+network:
+  ethernets:
+    eno1:
+      addresses:
+        # name=ditto
+        - 10.2.0.133/16
+        # name=mqtt1 etc
+        - 10.2.0.11/16
+        - 10.2.0.12/16
+        - 10.2.0.13/16
+        - 10.2.0.14/16
+      routes:
+        - to: default
+          via: 10.2.0.3
+      nameservers:
+        addresses: [10.2.0.3]
+    enp5s0:
+      dhcp4: true
+    ens3:
+      dhcp4: true
+  version: 2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/files/house_net.service	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,14 @@
+# written by pyinfra
+
+[Unit]
+After=network-online.target nss-lookup.target
+Wants=network-online.target nss-lookup.target
+
+[Service]
+Type=oneshot
+ExecStart=sh -c "sysctl net.ipv4.ip_forward=1 && /usr/sbin/iptables -A POSTROUTING --table nat --out-interface eth0 --jump MASQUERADE"
+RemainAfterExit=yes
+
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/files/pipe_10.2.network	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,14 @@
+# written by pyinfra
+
+[Match]
+# usb dongle
+MACAddress=00:05:1b:33:3e:81
+
+[Network]
+DHCP=no
+Address=10.2.0.3/16
+# vip for the filtered dns server we give clients who are to have sometimes-filtered domains
+Address=10.2.0.4/16
+DNS=10.2.0.3
+Domains=bigasterisk.com
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/files/pipe_isp.network	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,12 @@
+# written by pyinfra
+
+[Match]
+# onboard eth
+MACAddress=00:1e:06:43:20:d0
+
+[Network]
+DHCP=no
+Address=192.168.42.3/24
+Gateway=192.168.42.1
+DNS=10.2.0.1
+Domains=bigasterisk.com
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/files/prime.network	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,14 @@
+# written by pyinfra
+
+# see systemd.network(5)
+
+[Match]
+MACAddress=04:01:09:7f:89:01
+
+[Network]
+Address=162.243.138.136/24
+Gateway=162.243.138.1
+DNS=10.5.0.1%wg0
+DNS=8.8.8.8
+DNS=8.8.4.4
+Domains=bigasterisk.com
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/net.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,61 @@
+from pyinfra.context import host
+from pyinfra.operations import apt, files, server, systemd
+
+
+def no_ufw():
+    # On pipe:
+    #   Now using a HW router for this firewall. No incoming connections.
+    #   test connections from the outside:
+    #   http://www.t1shopper.com/tools/port-scanner/
+    # On prime:
+    #   using digitalocean network config:
+    #   https://cloud.digitalocean.com/networking/firewalls/f68899ae-1aac-4469-b379-59ce2bbc988f/droplets?i=7c5072
+    apt.packages(packages=['ufw'], present=False)
+
+
+def iptables_version():
+    # https://github.com/k3s-io/k3s/issues/1812 unclear, but more importantly, this has to be set
+    # on pipe in a way that works with the commands in house_net.service (and net_routes)
+    server.shell(commands=[
+        'update-alternatives --set iptables /usr/sbin/iptables-legacy',
+        'update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy',
+    ])
+    # needs reboot if this changed
+
+
+def no_ipv6():
+    server.sysctl(key='net.ipv6.conf.all.disable_ipv6', value=1, persist=True)
+
+
+def special_configs():
+    if host.name == 'prime':
+        files.template(
+            src="net/files/prime.network",
+            dest="/etc/systemd/network/99-prime.network",
+        )
+
+    elif host.name == 'pipe':
+        files.template(src="net/files/pipe_10.2.network", dest="/etc/systemd/network/99-10.2.network")
+        files.template(src="net/files/pipe_isp.network", dest="/etc/systemd/network/99-isp.network")
+        server.sysctl(key='net.ipv4.ip_forward', value=1, persist=True)
+        files.template(src="net/files/house_net.service", dest="/etc/systemd/system/house_net.service", out_interface='eth0')
+        systemd.service(service='house_net.service', daemon_reload=True, enabled=True, running=True, restarted=True)
+
+    elif host.name == 'ditto':
+        files.template(
+            src="net/files/ditto-netplan.yaml",
+            dest="/etc/netplan/00-installer-config.yaml",
+            create_remote_dir=True,
+        )
+
+    else:
+        return
+    systemd.service(service='systemd-networkd.service', enabled=True, running=True, restarted=True)
+
+
+operations = [
+    no_ufw,
+    iptables_version,
+    no_ipv6,
+    special_configs,
+]
--- a/package_lists.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,312 +0,0 @@
-# nodejs+friends are handled outside this file
-
-setup = [
-    'atool',
-    'build-essential',
-    'curl',
-    'iproute2',  # needed for wireguard
-    'iptables',
-    'mosquitto-clients',
-    'mtail',
-    'rsync',
-    'udns-utils',
-    'vim-tiny',
-    'wireguard-tools',
-]
-
-pi_setup = [
-    'apt-utils',
-    'dirmngr',
-    'gnupg2',
-    'pigpiod',
-]
-# something in here tries to get libflac8 & libpulse0 on pipe, which it doesn't have
-general = [
-    'apt-listchanges',
-    'aptitude',
-    'atool',
-    'bsd-mailx',
-    'fdisk',
-    'flatpak',
-    'hdparm',
-    'isc-dhcp-client',
-    'jq',
-    'keychain',
-    'kitty-terminfo',
-    'less',
-    'libgraphviz-dev',
-    'links',
-    'lua5.3',
-    'mercurial',
-    'moreutils',
-    'netcat-openbsd',
-    'ntpdate',
-    'rename',
-    'sshfs',
-    'vim-nox',
-    'wakeonlan',
-    'wget',
-    'wireguard',
-    'xosview',
-    'zsh',
-]
-
-non_pi = [
-    'emacs',
-    'lpr',
-    'nfs-client',
-    'python3-dulwich',  # desired, but it may depend on an old python3
-    'python3-atomicwrites',
-    'python3-invoke',
-    'python3-pip',
-    'python3-venv',
-    'python3-virtualenv',
-    'rclone',
-]
-
-debug = [
-    'debian-goodies',
-    'dmidecode',
-    'dstat',
-    'ethtool',
-    'gdb',
-    'hdparm',
-    'htop',
-    'ifstat',
-    'iotop',
-    'iproute2',
-    'lshw',
-    'lsof',
-    'mtr-tiny',
-    'ncdu',
-    'net-tools',
-    'nmap',
-    'oping',
-    'screen',
-    'smartmontools',
-    'speedtest-cli',
-    'strace',
-    'sysstat',
-    'tcpdump',
-    'wakeonlan',
-]
-
-for_ditto = [
-    'dnsmasq',
-    'nfs-common',
-    'openntpd',
-    'zfs-auto-snapshot',
-    'zfs-zed',
-    'zfsutils-linux',
-    'tgt',
-    #'libedgetpu1-std',  # for coral? not working on bang
-]
-
-for_pipe = [
-    'dnsmasq',
-    'python3-iptables',
-    'open-iscsi',
-]
-
-for_prime = [
-    'opendkim',
-    'opendkim-tools',
-]
-
-laptop = [
-    'brightnessctl',
-    'i3',
-    'network-manager',
-    'xserver-xorg-input-synaptics',
-    'tlp',
-]
-
-audio_plugins = [
-    'amb-plugins',
-    'ambdec',
-    'autotalent',
-    'blepvco',
-    'blop',
-    'bs2b-ladspa',
-    'caps',
-    'cmt',
-    'csladspa',
-    'dpf-plugins-ladspa',
-    'fil-plugins',
-    'guitarix-ladspa',
-    'invada-studio-plugins-ladspa',
-    'ladspa-sdk',
-    'lsp-plugins-ladspa',
-    'mcp-plugins',
-    'omins',
-    'rev-plugins',
-    'ste-plugins',
-    'swh-plugins',
-    'tap-plugins',
-    'vco-plugins',
-    'wah-plugins',
-]
-
-desktop = [
-    'adwaita-icon-theme-full',
-    'alsa-utils',
-    'apache2-utils',
-    'arandr',
-    'ardour',
-    'audacity',
-    'brasero',
-    'breeze',
-    'brightnessctl',
-    'checkinstall',
-    'cmake',
-    'code',
-    'cups',
-    'dclock',
-    'dolphin',
-    'eog',
-    'evtest',
-    'eye',
-    'fatrace',
-    'firefox',
-    'flameshot',
-    'fontmatrix',
-    'fonts-dejavu-core',
-    'fonts-dejavu-extra',
-    'fonts-dejavu',
-    'fonts-droid-fallback',
-    'fonts-emojione',
-    'fonts-font-awesome',
-    'fonts-freefont-ttf',
-    'fonts-lato',
-    'fonts-liberation2',
-    'fonts-noto',
-    'fonts-opensymbol',
-    'fonts-quicksand',
-    'fonts-texgyre',
-    'fonts-ubuntu-console',
-    'fonts-ubuntu',
-    'fonts-urw-base35',
-    'fvwm3',
-    'gdb',
-    'gedit',
-    'gimp-data-extras',
-    'gimp-gmic',
-    'gimp-plugin-registry',
-    'gimp-texturize',
-    'gimp',
-    'gnome-icon-theme',
-    'gnumeric',
-    'gnuplot',
-    'golang',
-    'google-chrome-stable',
-    'google-chrome-unstable',
-    'gstreamer1.0-libav',
-    'gstreamer1.0-opencv',
-    'gstreamer1.0-plugins-bad',
-    'gstreamer1.0-plugins-ugly',
-    'gstreamer1.0-tools',
-    'gstreamer1.0-vaapi',
-    'heif-gdk-pixbuf',
-    'heif-thumbnailer',
-    'humanity-icon-theme',
-    'i3lock',
-    'imagemagick',
-    'imwheel',
-    'jq',
-    'k4dirstat',
-    'libheif-examples',
-    'libreoffice-draw',
-    'libreoffice-impress',
-    'libreoffice-writer',
-    'libfuse2',  # for obsidian (appimage)
-    'libxcb-xkb1',  # needed for kitty
-    'lxterminal',
-    'meld',
-    'mpv',
-    'nmap',
-    'nodm',
-    'okular',
-    'pavucontrol',
-    'pamixer',
-    'pipewire-audio',
-    'python3-dulwich',
-    'python3-evemu',
-    'python3-opencv',
-    'python3-pycurl',
-    'python3-rdflib',
-    'python3-venv',
-    'qjackctl',
-    'qv4l2',
-    'rar',
-    'rclone',
-    'recordmydesktop',
-    'simple-scan',
-    'simplescreenrecorder',
-    'solvespace',
-    'sqlitebrowser',
-    'sshfs',
-    'steam-launcher',
-    'swi-prolog',
-    'syncthing-gtk', # this may pull old ubu syncthing version, which sync.py replaces
-    'system-config-printer',
-    'systemd-resolved',
-    'trayer',
-    'ttf-bitstream-vera',
-    'visidata',
-    'vlc',
-    'wamerican',
-    'wireshark',
-    'wmctrl',
-    'x11-apps',
-    'x11vnc',
-    'xclip',
-    'xfonts-base',
-    'xfonts-encodings',
-    'xfonts-utils',
-    'xpad',
-    'xsane',
-    'xterm',
-    'xtightvncviewer',
-    'xvfb',
-    'libssl-dev',  # for pypi 'packages'
-    'libcurl4-openssl-dev',  # for pypi 'packages'
-    'kicad',
-    'openscad',
-    'dunst',
-    'gmic',
-    'git-cola',
-    'optipng',
-    'pngcrush',
-    'pngquant',
-    'cdparanoia',
-]
-
-xorg = [
-    'kbd',
-    'nvidia-modprobe',
-    'nvidia-prime',
-    'nvidia-settings',
-    'screen-resolution-extra',
-    'xserver-xorg',
-]
-
-
-def k8s_node_with_nvidia_gpu(hostName):
-    version = {
-        'dash': '550',
-        'dot': '550',  # just not updated yet
-        'slash': '550',
-        'ditto': '550-server',
-        # 'bang': '390-server',  # no longer in ubuntu
-        'squib': '470',  # held back for old gfx card
-    }[hostName]
-    return [
-        'nvidia-container-runtime',
-        f'nvidia-headless-{version}',
-        f'nvidia-utils-{version}',
-        f'libnvidia-decode-{version}',
-        f'libnvidia-encode-{version}',
-        f'nvidia-driver-{version}',
-    ] + ([] if 'server' in version else [
-        f'xserver-xorg-video-nvidia-{version}',
-    ])
--- a/packages.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,153 +0,0 @@
-from io import StringIO
-
-from pyinfra import host
-from pyinfra.operations import apt, files, server, systemd
-
-import package_lists
-
-
-def kitty():
-    apt.packages(packages=['kitty'], present=False, force=True)
-    vers = '0.36.2'  # see https://github.com/kovidgoyal/kitty/releases
-    home = '/home/drewp'
-    local = f"{home}/.local/kitty"
-    dl = f'/tmp/kitty-{vers}-x86_64.txz'
-    files.download(src=f"https://github.com/kovidgoyal/kitty/releases/download/v{vers}/kitty-{vers}-x86_64.txz", dest=dl)
-    files.directory(path=local)
-    server.shell(commands=[
-        f"mkdir -p {local}",  # https://github.com/Fizzadar/pyinfra/issues/777
-        f"aunpack --extract-to={local} {dl}",
-    ])
-    files.link(target="{local}/bin/kitty", path="{home}/bin/kitty")
-    files.link(target="{local}/bin/kitten", path="{home}/bin/kitten")
-
-
-def nodejs():
-    apt.packages(packages=['libnode72'], present=False, force=True)
-    apt.packages(packages=['nodejs'], latest=True)
-    server.shell(commands=[
-        "rm -f /usr/local/bin/pnp{m,x}",
-        "corepack enable",
-        # https://github.com/pnpm/pnpm/releases
-        # but also https://pnpm.io/installation#compatibility
-        "corepack prepare 'pnpm@9.8' --activate",
-    ])
-
-
-def podman():
-    # frigate build wants to mount a single file from the host, which needs podman 4.5.1
-    # https://github.com/containers/podman/issues/12123#issuecomment-1620439593
-    server.shell(commands='apt --fix-broken install')
-    apt.deb(src="http://ftp.osuosl.org/pub/ubuntu/pool/main/g/gpgme1.0/libgpgme11t64_1.18.0-4.1ubuntu4_amd64.deb")
-    server.shell(commands='apt --fix-broken install')
-    apt.deb(src="http://ftp.osuosl.org/pub/ubuntu/pool/universe/c/conmon/conmon_2.1.10+ds1-1build2_amd64.deb")
-    apt.deb(src="http://ftp.osuosl.org/pub/ubuntu/pool/universe/libp/libpod/podman_4.9.3+ds1-1build2_amd64.deb")
-    apt.packages(packages=['libsubid4', 'buildah', 'podman-docker'], latest=True)
-
-
-def pdm():
-    # https://github.com/pdm-project/pdm/blob/main/CHANGELOG.md
-    server.shell(commands=["pip install --break-system-packages 'pdm==2.21.0'"])
-
-
-def proper_locate():
-    apt.packages(packages='mlocate', present=False)
-    if 'pi' not in host.groups and host.name not in ['prime', 'pipe']:
-        apt.packages(packages='plocate')
-
-
-def proper_man():
-    if 'small' in host.groups or 'pi' in host.groups:
-        apt.packages(packages=['mandb'], present=False)
-
-
-def no_unwanted_services():
-    systemd.service(service='nginx', enabled=False, running=False)
-
-
-apt.packages(packages=package_lists.setup, latest=True)
-
-
-def roblox():
-    server.shell(commands='flatpak install -y org.freedesktop.Platform/x86_64/23.08')
-    server.shell(commands='flatpak install -y flathub org.vinegarhq.Vinegar')  # (roblox runner)
-    files.put(
-        src=StringIO(
-            #"#!/bin/sh\nexec flatpak run org.vinegarhq.Vinegar player run 'roblox-player:1'\n"
-            "#!/bin/sh\n exec /usr/bin/flatpak run --branch=stable --arch=x86_64 --command=vinegar org.vinegarhq.Vinegar player run -app\n"
-        ),
-        dest='/usr/local/bin/roblox.real',
-        mode='755')
-
-    for desktopFile in [
-            '/var/lib/flatpak/exports/share/applications/org.vinegarhq.Vinegar.app.desktop',
-            '/var/lib/flatpak/app/org.vinegarhq.Vinegar/current/active/export/share/applications/org.vinegarhq.Vinegar.player.desktop',
-    ]:
-        files.line(path=desktopFile, line="^Exec", replace='Exec=/usr/local/bin/roblox')
-    files.link(target='/usr/local/bin/run_while_allowed', path='/usr/local/bin/roblox', force=True)
-
-
-def kube_node():
-
-    # avoid having to this workaround:
-    # https://longhorn.io/kb/troubleshooting-volume-with-multipath/
-    apt.packages(packages=['multipath-tools'], force=True, present=False)
-
-    apt.packages(packages=[
-        # https://longhorn.io/docs/1.6.1/deploy/install/#installation-requirements
-        'open-iscsi',
-        'nfs-common',
-        'cryptsetup',
-    ])
-
-
-proper_locate()
-proper_man()
-nodejs()
-
-apt.packages(packages=package_lists.general, latest=True)
-apt.packages(packages=package_lists.debug, latest=True)
-
-if host.name == "pipe":
-    apt.packages(packages=package_lists.for_pipe, latest=True)
-
-if host.name != 'pipe':
-    apt.packages(packages=['reptyr'])
-
-if host.name == "prime":
-    apt.packages(packages=package_lists.for_prime, latest=True)
-
-if host.name == 'plus':
-    apt.packages(packages=package_lists.laptop, latest=True)
-
-if host.data.get('gpu'):
-    apt.packages(packages=package_lists.k8s_node_with_nvidia_gpu(host.name))
-
-if host.data.get('k8s_admin'):
-    podman()
-
-is_kube_node = host.name in ['dash', 'slash', 'ditto', 'ws-printer', 'li-drums']
-if is_kube_node:
-    kube_node()
-    
-if host.name == 'ditto':
-    apt.packages(packages=package_lists.for_ditto, latest=True)
-
-if 'pi' not in host.groups:
-    kitty()
-    apt.packages(packages=package_lists.non_pi, latest=True)
-
-if 'pi' in host.groups:
-    apt.packages(packages=package_lists.pi_setup)
-
-desktop_env = host.name in ['dash', 'slash', 'plus', 'dot', 'squib', 'pillow', 'tofu']
-if desktop_env:
-    apt.packages(packages=package_lists.xorg + package_lists.desktop, latest=True)
-    # broken, per https://vinegarhq.org/
-    # roblox()
-if desktop_env or host.name in ['bang', 'ditto']:
-    pdm()
-
-no_unwanted_services()
-
-# todo: ./mrv2-v1.0.8-Linux-amd64.deb
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/package_lists.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,312 @@
+# nodejs+friends are handled outside this file
+
+setup = [
+    'atool',
+    'build-essential',
+    'curl',
+    'iproute2',  # needed for wireguard
+    'iptables',
+    'mosquitto-clients',
+    'mtail',
+    'rsync',
+    'udns-utils',
+    'vim-tiny',
+    'wireguard-tools',
+]
+
+pi_setup = [
+    'apt-utils',
+    'dirmngr',
+    'gnupg2',
+    'pigpiod',
+]
+# something in here tries to get libflac8 & libpulse0 on pipe, which it doesn't have
+general = [
+    'apt-listchanges',
+    'aptitude',
+    'atool',
+    'bsd-mailx',
+    'fdisk',
+    'flatpak',
+    'hdparm',
+    'isc-dhcp-client',
+    'jq',
+    'keychain',
+    'kitty-terminfo',
+    'less',
+    'libgraphviz-dev',
+    'links',
+    'lua5.3',
+    'mercurial',
+    'moreutils',
+    'netcat-openbsd',
+    'ntpdate',
+    'rename',
+    'sshfs',
+    'vim-nox',
+    'wakeonlan',
+    'wget',
+    'wireguard',
+    'xosview',
+    'zsh',
+]
+
+non_pi = [
+    'emacs',
+    'lpr',
+    'nfs-client',
+    'python3-dulwich',  # desired, but it may depend on an old python3
+    'python3-atomicwrites',
+    'python3-invoke',
+    'python3-pip',
+    'python3-venv',
+    'python3-virtualenv',
+    'rclone',
+]
+
+debug = [
+    'debian-goodies',
+    'dmidecode',
+    'dstat',
+    'ethtool',
+    'gdb',
+    'hdparm',
+    'htop',
+    'ifstat',
+    'iotop',
+    'iproute2',
+    'lshw',
+    'lsof',
+    'mtr-tiny',
+    'ncdu',
+    'net-tools',
+    'nmap',
+    'oping',
+    'screen',
+    'smartmontools',
+    'speedtest-cli',
+    'strace',
+    'sysstat',
+    'tcpdump',
+    'wakeonlan',
+]
+
+for_ditto = [
+    'dnsmasq',
+    'nfs-common',
+    'openntpd',
+    'zfs-auto-snapshot',
+    'zfs-zed',
+    'zfsutils-linux',
+    'tgt',
+    #'libedgetpu1-std',  # for coral? not working on bang
+]
+
+for_pipe = [
+    'dnsmasq',
+    'python3-iptables',
+    'open-iscsi',
+]
+
+for_prime = [
+    'opendkim',
+    'opendkim-tools',
+]
+
+laptop = [
+    'brightnessctl',
+    'i3',
+    'network-manager',
+    'xserver-xorg-input-synaptics',
+    'tlp',
+]
+
+audio_plugins = [
+    'amb-plugins',
+    'ambdec',
+    'autotalent',
+    'blepvco',
+    'blop',
+    'bs2b-ladspa',
+    'caps',
+    'cmt',
+    'csladspa',
+    'dpf-plugins-ladspa',
+    'fil-plugins',
+    'guitarix-ladspa',
+    'invada-studio-plugins-ladspa',
+    'ladspa-sdk',
+    'lsp-plugins-ladspa',
+    'mcp-plugins',
+    'omins',
+    'rev-plugins',
+    'ste-plugins',
+    'swh-plugins',
+    'tap-plugins',
+    'vco-plugins',
+    'wah-plugins',
+]
+
+desktop = [
+    'adwaita-icon-theme-full',
+    'alsa-utils',
+    'apache2-utils',
+    'arandr',
+    'ardour',
+    'audacity',
+    'brasero',
+    'breeze',
+    'brightnessctl',
+    'checkinstall',
+    'cmake',
+    'code',
+    'cups',
+    'dclock',
+    'dolphin',
+    'eog',
+    'evtest',
+    'eye',
+    'fatrace',
+    'firefox',
+    'flameshot',
+    'fontmatrix',
+    'fonts-dejavu-core',
+    'fonts-dejavu-extra',
+    'fonts-dejavu',
+    'fonts-droid-fallback',
+    'fonts-emojione',
+    'fonts-font-awesome',
+    'fonts-freefont-ttf',
+    'fonts-lato',
+    'fonts-liberation2',
+    'fonts-noto',
+    'fonts-opensymbol',
+    'fonts-quicksand',
+    'fonts-texgyre',
+    'fonts-ubuntu-console',
+    'fonts-ubuntu',
+    'fonts-urw-base35',
+    'fvwm3',
+    'gdb',
+    'gedit',
+    'gimp-data-extras',
+    'gimp-gmic',
+    'gimp-plugin-registry',
+    'gimp-texturize',
+    'gimp',
+    'gnome-icon-theme',
+    'gnumeric',
+    'gnuplot',
+    'golang',
+    'google-chrome-stable',
+    'google-chrome-unstable',
+    'gstreamer1.0-libav',
+    'gstreamer1.0-opencv',
+    'gstreamer1.0-plugins-bad',
+    'gstreamer1.0-plugins-ugly',
+    'gstreamer1.0-tools',
+    'gstreamer1.0-vaapi',
+    'heif-gdk-pixbuf',
+    'heif-thumbnailer',
+    'humanity-icon-theme',
+    'i3lock',
+    'imagemagick',
+    'imwheel',
+    'jq',
+    'k4dirstat',
+    'libheif-examples',
+    'libreoffice-draw',
+    'libreoffice-impress',
+    'libreoffice-writer',
+    'libfuse2',  # for obsidian (appimage)
+    'libxcb-xkb1',  # needed for kitty
+    'lxterminal',
+    'meld',
+    'mpv',
+    'nmap',
+    'nodm',
+    'okular',
+    'pavucontrol',
+    'pamixer',
+    'pipewire-audio',
+    'python3-dulwich',
+    'python3-evemu',
+    'python3-opencv',
+    'python3-pycurl',
+    'python3-rdflib',
+    'python3-venv',
+    'qjackctl',
+    'qv4l2',
+    'rar',
+    'rclone',
+    'recordmydesktop',
+    'simple-scan',
+    'simplescreenrecorder',
+    'solvespace',
+    'sqlitebrowser',
+    'sshfs',
+    'steam-launcher',
+    'swi-prolog',
+    'syncthing-gtk', # this may pull old ubu syncthing version, which sync.py replaces
+    'system-config-printer',
+    'systemd-resolved',
+    'trayer',
+    'ttf-bitstream-vera',
+    'visidata',
+    'vlc',
+    'wamerican',
+    'wireshark',
+    'wmctrl',
+    'x11-apps',
+    'x11vnc',
+    'xclip',
+    'xfonts-base',
+    'xfonts-encodings',
+    'xfonts-utils',
+    'xpad',
+    'xsane',
+    'xterm',
+    'xtightvncviewer',
+    'xvfb',
+    'libssl-dev',  # for pypi 'packages'
+    'libcurl4-openssl-dev',  # for pypi 'packages'
+    'kicad',
+    'openscad',
+    'dunst',
+    'gmic',
+    'git-cola',
+    'optipng',
+    'pngcrush',
+    'pngquant',
+    'cdparanoia',
+]
+
+xorg = [
+    'kbd',
+    'nvidia-modprobe',
+    'nvidia-prime',
+    'nvidia-settings',
+    'screen-resolution-extra',
+    'xserver-xorg',
+]
+
+
+def k8s_node_with_nvidia_gpu(hostName):
+    version = {
+        'dash': '550',
+        'dot': '550',  # just not updated yet
+        'slash': '550',
+        'ditto': '550-server',
+        # 'bang': '390-server',  # no longer in ubuntu
+        'squib': '470',  # held back for old gfx card
+    }[hostName]
+    return [
+        'nvidia-container-runtime',
+        f'nvidia-headless-{version}',
+        f'nvidia-utils-{version}',
+        f'libnvidia-decode-{version}',
+        f'libnvidia-encode-{version}',
+        f'nvidia-driver-{version}',
+    ] + ([] if 'server' in version else [
+        f'xserver-xorg-video-nvidia-{version}',
+    ])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/packages/packages.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,158 @@
+from io import StringIO
+
+from pyinfra.context import host
+from pyinfra.operations import apt, files, server, systemd
+
+import packages.package_lists as package_lists
+
+
+def kitty():
+    apt.packages(packages=['kitty'], present=False, force=True)
+    vers = '0.36.2'  # see https://github.com/kovidgoyal/kitty/releases
+    home = '/home/drewp'
+    local = f"{home}/.local/kitty"
+    dl = f'/tmp/kitty-{vers}-x86_64.txz'
+    files.download(src=f"https://github.com/kovidgoyal/kitty/releases/download/v{vers}/kitty-{vers}-x86_64.txz", dest=dl)
+    files.directory(path=local)
+    server.shell(commands=[
+        f"mkdir -p {local}",  # https://github.com/Fizzadar/pyinfra/issues/777
+        f"aunpack --extract-to={local} {dl}",
+    ])
+    files.link(target="{local}/bin/kitty", path="{home}/bin/kitty")
+    files.link(target="{local}/bin/kitten", path="{home}/bin/kitten")
+
+
+def nodejs():
+    if 'pi' in host.groups or host.name == 'pipe' or host.name == 'prime':
+        return
+    apt.packages(packages=['libnode72'], present=False, force=True)
+    apt.packages(packages=['nodejs'], latest=True)
+    server.shell(commands=[
+        "rm -f /usr/local/bin/pnp{m,x}",
+        "corepack enable",
+        # https://github.com/pnpm/pnpm/releases
+        # but also https://pnpm.io/installation#compatibility
+        "corepack prepare 'pnpm@9.8' --activate",
+    ])
+
+
+def podman():
+    # frigate build wants to mount a single file from the host, which needs podman 4.5.1
+    # https://github.com/containers/podman/issues/12123#issuecomment-1620439593
+    server.shell(commands='apt --fix-broken install')
+    apt.deb(src="http://ftp.osuosl.org/pub/ubuntu/pool/main/g/gpgme1.0/libgpgme11t64_1.18.0-4.1ubuntu4_amd64.deb")
+    server.shell(commands='apt --fix-broken install')
+    apt.deb(src="http://ftp.osuosl.org/pub/ubuntu/pool/universe/c/conmon/conmon_2.1.10+ds1-1build2_amd64.deb")
+    apt.deb(src="http://ftp.osuosl.org/pub/ubuntu/pool/universe/libp/libpod/podman_4.9.3+ds1-1build2_amd64.deb")
+    apt.packages(packages=['libsubid4', 'buildah', 'podman-docker'], latest=True)
+
+
+def pdm():
+    # https://github.com/pdm-project/pdm/blob/main/CHANGELOG.md
+    server.shell(commands=["pip install --break-system-packages 'pdm==2.21.0'"])
+
+
+def proper_locate():
+    apt.packages(packages='mlocate', present=False)
+    if 'pi' not in host.groups and host.name not in ['prime', 'pipe']:
+        apt.packages(packages='plocate')
+
+
+def proper_man():
+    if 'small' in host.groups or 'pi' in host.groups:
+        apt.packages(packages=['mandb'], present=False)
+
+
+def no_unwanted_services():
+    systemd.service(service='nginx', enabled=False, running=False)
+
+
+apt.packages(packages=package_lists.setup, latest=True)
+
+
+def roblox():
+    server.shell(commands='flatpak install -y org.freedesktop.Platform/x86_64/23.08')
+    server.shell(commands='flatpak install -y flathub org.vinegarhq.Vinegar')  # (roblox runner)
+    files.put(
+        src=StringIO(
+            #"#!/bin/sh\nexec flatpak run org.vinegarhq.Vinegar player run 'roblox-player:1'\n"
+            "#!/bin/sh\n exec /usr/bin/flatpak run --branch=stable --arch=x86_64 --command=vinegar org.vinegarhq.Vinegar player run -app\n"
+        ),
+        dest='/usr/local/bin/roblox.real',
+        mode='755')
+
+    for desktopFile in [
+            '/var/lib/flatpak/exports/share/applications/org.vinegarhq.Vinegar.app.desktop',
+            '/var/lib/flatpak/app/org.vinegarhq.Vinegar/current/active/export/share/applications/org.vinegarhq.Vinegar.player.desktop',
+    ]:
+        files.line(path=desktopFile, line="^Exec", replace='Exec=/usr/local/bin/roblox')
+    files.link(target='/usr/local/bin/run_while_allowed', path='/usr/local/bin/roblox', force=True)
+
+
+def kube_node():
+
+    # avoid having to this workaround:
+    # https://longhorn.io/kb/troubleshooting-volume-with-multipath/
+    apt.packages(packages=['multipath-tools'], force=True, present=False)
+
+    apt.packages(packages=[
+        # https://longhorn.io/docs/1.6.1/deploy/install/#installation-requirements
+        'open-iscsi',
+        'nfs-common',
+        'cryptsetup',
+    ])
+
+
+def install_packages():
+    apt.packages(packages=package_lists.general, latest=True)
+    apt.packages(packages=package_lists.debug, latest=True)
+
+    if host.name == "pipe":
+        apt.packages(packages=package_lists.for_pipe, latest=True)
+
+    if host.name != 'pipe':
+        apt.packages(packages=['reptyr'])
+
+    if host.name == "prime":
+        apt.packages(packages=package_lists.for_prime, latest=True)
+
+    if host.name == 'plus':
+        apt.packages(packages=package_lists.laptop, latest=True)
+
+    if host.data.get('gpu'):
+        apt.packages(packages=package_lists.k8s_node_with_nvidia_gpu(host.name))
+
+    if host.data.get('k8s_admin'):
+        podman()
+
+    is_kube_node = host.name in ['dash', 'slash', 'ditto', 'ws-printer', 'li-drums']
+    if is_kube_node:
+        kube_node()
+
+    if host.name == 'ditto':
+        apt.packages(packages=package_lists.for_ditto, latest=True)
+
+    if 'pi' not in host.groups:
+        kitty()
+        apt.packages(packages=package_lists.non_pi, latest=True)
+
+    if 'pi' in host.groups:
+        apt.packages(packages=package_lists.pi_setup)
+
+    desktop_env = host.name in ['dash', 'slash', 'plus', 'dot', 'squib', 'pillow', 'tofu']
+    if desktop_env:
+        apt.packages(packages=package_lists.xorg + package_lists.desktop, latest=True)
+        # broken, per https://vinegarhq.org/
+        # roblox()
+    if desktop_env or host.name in ['bang', 'ditto']:
+        pdm()
+
+
+operations = [
+    proper_locate,
+    proper_man,
+    nodejs,
+    install_packages,
+    no_unwanted_services,
+]
+# todo: ./mrv2-v1.0.8-Linux-amd64.deb
--- a/pdm.lock	Mon Jan 20 14:10:19 2025 -0800
+++ b/pdm.lock	Mon Jan 20 21:55:08 2025 -0800
@@ -5,57 +5,57 @@
 groups = ["default"]
 strategy = ["inherit_metadata"]
 lock_version = "4.5.0"
-content_hash = "sha256:9781fcf43396e51303cb5dbdd0f03e03c15e7a8d9a715453d6d86573238cb9df"
+content_hash = "sha256:2f3513e59e107ea4c3b5e84ef47ba0525373e5783d6e35047a2c694312cbc1c5"
 
 [[metadata.targets]]
 requires_python = ">=3.11"
 
 [[package]]
 name = "bcrypt"
-version = "4.2.0"
+version = "4.2.1"
 requires_python = ">=3.7"
 summary = "Modern password hashing for your software and your servers"
 groups = ["default"]
 files = [
-    {file = "bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb"},
-    {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00"},
-    {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d"},
-    {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291"},
-    {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328"},
-    {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7"},
-    {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399"},
-    {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060"},
-    {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7"},
-    {file = "bcrypt-4.2.0-cp37-abi3-win32.whl", hash = "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458"},
-    {file = "bcrypt-4.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5"},
-    {file = "bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841"},
-    {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68"},
-    {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe"},
-    {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2"},
-    {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c"},
-    {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae"},
-    {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d"},
-    {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e"},
-    {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8"},
-    {file = "bcrypt-4.2.0-cp39-abi3-win32.whl", hash = "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34"},
-    {file = "bcrypt-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9"},
-    {file = "bcrypt-4.2.0.tar.gz", hash = "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221"},
+    {file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"},
+    {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"},
+    {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad"},
+    {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea"},
+    {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396"},
+    {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425"},
+    {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685"},
+    {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6"},
+    {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139"},
+    {file = "bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005"},
+    {file = "bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526"},
+    {file = "bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413"},
+    {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a"},
+    {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c"},
+    {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99"},
+    {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54"},
+    {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837"},
+    {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331"},
+    {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84"},
+    {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d"},
+    {file = "bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf"},
+    {file = "bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c"},
+    {file = "bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe"},
 ]
 
 [[package]]
 name = "certifi"
-version = "2024.7.4"
+version = "2024.12.14"
 requires_python = ">=3.6"
 summary = "Python package for providing Mozilla's CA Bundle."
 groups = ["default"]
 files = [
-    {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
-    {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
+    {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"},
+    {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"},
 ]
 
 [[package]]
 name = "cffi"
-version = "1.17.0"
+version = "1.17.1"
 requires_python = ">=3.8"
 summary = "Foreign Function Interface for Python calling C code."
 groups = ["default"]
@@ -63,87 +63,96 @@
     "pycparser",
 ]
 files = [
-    {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"},
-    {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"},
-    {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"},
-    {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"},
-    {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"},
-    {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"},
-    {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"},
-    {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"},
-    {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"},
-    {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"},
-    {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"},
-    {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"},
-    {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"},
-    {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"},
-    {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"},
-    {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"},
-    {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"},
-    {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"},
-    {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"},
-    {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"},
-    {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"},
-    {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"},
-    {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"},
-    {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"},
-    {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"},
-    {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"},
-    {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"},
-    {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"},
-    {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"},
-    {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"},
-    {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"},
-    {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"},
-    {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"},
-    {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"},
-    {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"},
+    {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
+    {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
+    {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
+    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
+    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
+    {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
+    {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
+    {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
+    {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
+    {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
+    {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
+    {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
+    {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
+    {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
+    {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
+    {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
+    {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
+    {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
+    {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
+    {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
+    {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
+    {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
+    {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
 ]
 
 [[package]]
 name = "charset-normalizer"
-version = "3.3.2"
-requires_python = ">=3.7.0"
+version = "3.4.1"
+requires_python = ">=3.7"
 summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 groups = ["default"]
 files = [
-    {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
-    {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
-    {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
-    {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"},
+    {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"},
+    {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"},
+    {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"},
+    {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"},
+    {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"},
 ]
 
 [[package]]
 name = "click"
-version = "8.1.7"
+version = "8.1.8"
 requires_python = ">=3.7"
 summary = "Composable command line interface toolkit"
 groups = ["default"]
@@ -152,8 +161,8 @@
     "importlib-metadata; python_version < \"3.8\"",
 ]
 files = [
-    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
-    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+    {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
+    {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
 ]
 
 [[package]]
@@ -169,45 +178,36 @@
 ]
 
 [[package]]
-name = "configparser"
-version = "7.1.0"
-requires_python = ">=3.8"
-summary = "Updated configparser from stdlib for earlier Pythons."
-groups = ["default"]
-files = [
-    {file = "configparser-7.1.0-py3-none-any.whl", hash = "sha256:98e374573c4e10e92399651e3ba1c47a438526d633c44ee96143dec26dad4299"},
-    {file = "configparser-7.1.0.tar.gz", hash = "sha256:eb82646c892dbdf773dae19c633044d163c3129971ae09b49410a303b8e0a5f7"},
-]
-
-[[package]]
 name = "cryptography"
-version = "43.0.0"
-requires_python = ">=3.7"
+version = "44.0.0"
+requires_python = "!=3.9.0,!=3.9.1,>=3.7"
 summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 groups = ["default"]
 dependencies = [
     "cffi>=1.12; platform_python_implementation != \"PyPy\"",
 ]
 files = [
-    {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"},
-    {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"},
-    {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"},
-    {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"},
-    {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"},
-    {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"},
-    {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"},
-    {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"},
-    {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"},
-    {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"},
-    {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"},
-    {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"},
-    {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"},
-    {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"},
-    {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"},
-    {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"},
-    {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"},
-    {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"},
-    {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"},
+    {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"},
+    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"},
+    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"},
+    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"},
+    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"},
+    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"},
+    {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"},
+    {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"},
+    {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"},
+    {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"},
+    {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"},
+    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"},
+    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"},
+    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"},
+    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"},
+    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"},
+    {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"},
+    {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"},
+    {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"},
+    {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"},
+    {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"},
 ]
 
 [[package]]
@@ -223,82 +223,114 @@
 
 [[package]]
 name = "gevent"
-version = "24.2.1"
-requires_python = ">=3.8"
+version = "24.11.1"
+requires_python = ">=3.9"
 summary = "Coroutine-based network library"
 groups = ["default"]
 dependencies = [
-    "cffi>=1.12.2; platform_python_implementation == \"CPython\" and sys_platform == \"win32\"",
-    "greenlet>=2.0.0; platform_python_implementation == \"CPython\" and python_version < \"3.11\"",
-    "greenlet>=3.0rc3; platform_python_implementation == \"CPython\" and python_version >= \"3.11\"",
+    "cffi>=1.17.1; platform_python_implementation == \"CPython\" and sys_platform == \"win32\"",
+    "greenlet>=3.1.1; platform_python_implementation == \"CPython\"",
     "zope-event",
     "zope-interface",
 ]
 files = [
-    {file = "gevent-24.2.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:03aa5879acd6b7076f6a2a307410fb1e0d288b84b03cdfd8c74db8b4bc882fc5"},
-    {file = "gevent-24.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8bb35ce57a63c9a6896c71a285818a3922d8ca05d150fd1fe49a7f57287b836"},
-    {file = "gevent-24.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7f87c2c02e03d99b95cfa6f7a776409083a9e4d468912e18c7680437b29222c"},
-    {file = "gevent-24.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968581d1717bbcf170758580f5f97a2925854943c45a19be4d47299507db2eb7"},
-    {file = "gevent-24.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7899a38d0ae7e817e99adb217f586d0a4620e315e4de577444ebeeed2c5729be"},
-    {file = "gevent-24.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f5e8e8d60e18d5f7fd49983f0c4696deeddaf6e608fbab33397671e2fcc6cc91"},
-    {file = "gevent-24.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fbfdce91239fe306772faab57597186710d5699213f4df099d1612da7320d682"},
-    {file = "gevent-24.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cdf66977a976d6a3cfb006afdf825d1482f84f7b81179db33941f2fc9673bb1d"},
-    {file = "gevent-24.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1dffb395e500613e0452b9503153f8f7ba587c67dd4a85fc7cd7aa7430cb02cc"},
-    {file = "gevent-24.2.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:6c47ae7d1174617b3509f5d884935e788f325eb8f1a7efc95d295c68d83cce40"},
-    {file = "gevent-24.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7cac622e11b4253ac4536a654fe221249065d9a69feb6cdcd4d9af3503602e0"},
-    {file = "gevent-24.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf5b9c72b884c6f0c4ed26ef204ee1f768b9437330422492c319470954bc4cc7"},
-    {file = "gevent-24.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5de3c676e57177b38857f6e3cdfbe8f38d1cd754b63200c0615eaa31f514b4f"},
-    {file = "gevent-24.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4faf846ed132fd7ebfbbf4fde588a62d21faa0faa06e6f468b7faa6f436b661"},
-    {file = "gevent-24.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:368a277bd9278ddb0fde308e6a43f544222d76ed0c4166e0d9f6b036586819d9"},
-    {file = "gevent-24.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f8a04cf0c5b7139bc6368b461257d4a757ea2fe89b3773e494d235b7dd51119f"},
-    {file = "gevent-24.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9d8d0642c63d453179058abc4143e30718b19a85cbf58c2744c9a63f06a1d388"},
-    {file = "gevent-24.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:94138682e68ec197db42ad7442d3cf9b328069c3ad8e4e5022e6b5cd3e7ffae5"},
-    {file = "gevent-24.2.1.tar.gz", hash = "sha256:432fc76f680acf7cf188c2ee0f5d3ab73b63c1f03114c7cd8a34cebbe5aa2056"},
+    {file = "gevent-24.11.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:351d1c0e4ef2b618ace74c91b9b28b3eaa0dd45141878a964e03c7873af09f62"},
+    {file = "gevent-24.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5efe72e99b7243e222ba0c2c2ce9618d7d36644c166d63373af239da1036bab"},
+    {file = "gevent-24.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d3b249e4e1f40c598ab8393fc01ae6a3b4d51fc1adae56d9ba5b315f6b2d758"},
+    {file = "gevent-24.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81d918e952954675f93fb39001da02113ec4d5f4921bf5a0cc29719af6824e5d"},
+    {file = "gevent-24.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c935b83d40c748b6421625465b7308d87c7b3717275acd587eef2bd1c39546"},
+    {file = "gevent-24.11.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff96c5739834c9a594db0e12bf59cb3fa0e5102fc7b893972118a3166733d61c"},
+    {file = "gevent-24.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d6c0a065e31ef04658f799215dddae8752d636de2bed61365c358f9c91e7af61"},
+    {file = "gevent-24.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:97e2f3999a5c0656f42065d02939d64fffaf55861f7d62b0107a08f52c984897"},
+    {file = "gevent-24.11.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:a3d75fa387b69c751a3d7c5c3ce7092a171555126e136c1d21ecd8b50c7a6e46"},
+    {file = "gevent-24.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beede1d1cff0c6fafae3ab58a0c470d7526196ef4cd6cc18e7769f207f2ea4eb"},
+    {file = "gevent-24.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85329d556aaedced90a993226d7d1186a539c843100d393f2349b28c55131c85"},
+    {file = "gevent-24.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:816b3883fa6842c1cf9d2786722014a0fd31b6312cca1f749890b9803000bad6"},
+    {file = "gevent-24.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b24d800328c39456534e3bc3e1684a28747729082684634789c2f5a8febe7671"},
+    {file = "gevent-24.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5f1701ce0f7832f333dd2faf624484cbac99e60656bfbb72504decd42970f0f"},
+    {file = "gevent-24.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d740206e69dfdfdcd34510c20adcb9777ce2cc18973b3441ab9767cd8948ca8a"},
+    {file = "gevent-24.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:68bee86b6e1c041a187347ef84cf03a792f0b6c7238378bf6ba4118af11feaae"},
+    {file = "gevent-24.11.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d618e118fdb7af1d6c1a96597a5cd6ac84a9f3732b5be8515c6a66e098d498b6"},
+    {file = "gevent-24.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2142704c2adce9cd92f6600f371afb2860a446bfd0be5bd86cca5b3e12130766"},
+    {file = "gevent-24.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92e0d7759de2450a501effd99374256b26359e801b2d8bf3eedd3751973e87f5"},
+    {file = "gevent-24.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca845138965c8c56d1550499d6b923eb1a2331acfa9e13b817ad8305dde83d11"},
+    {file = "gevent-24.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:356b73d52a227d3313f8f828025b665deada57a43d02b1cf54e5d39028dbcf8d"},
+    {file = "gevent-24.11.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:58851f23c4bdb70390f10fc020c973ffcf409eb1664086792c8b1e20f25eef43"},
+    {file = "gevent-24.11.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1ea50009ecb7f1327347c37e9eb6561bdbc7de290769ee1404107b9a9cba7cf1"},
+    {file = "gevent-24.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:ec68e270543ecd532c4c1d70fca020f90aa5486ad49c4f3b8b2e64a66f5c9274"},
+    {file = "gevent-24.11.1.tar.gz", hash = "sha256:8bd1419114e9e4a3ed33a5bad766afff9a3cf765cb440a582a1b3a9bc80c1aca"},
 ]
 
 [[package]]
 name = "greenlet"
-version = "3.0.3"
+version = "3.1.1"
 requires_python = ">=3.7"
 summary = "Lightweight in-process concurrent programming"
 groups = ["default"]
-marker = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\""
+marker = "platform_python_implementation == \"CPython\""
 files = [
-    {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"},
-    {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"},
-    {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"},
-    {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"},
-    {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"},
-    {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"},
-    {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"},
-    {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"},
-    {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"},
-    {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"},
-    {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"},
+    {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"},
+    {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"},
+    {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"},
+    {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"},
+    {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"},
+    {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"},
+    {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"},
+    {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"},
+    {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"},
+    {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"},
+    {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"},
+    {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"},
+    {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"},
+    {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"},
+    {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"},
+    {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"},
+    {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"},
+    {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"},
+    {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"},
 ]
 
 [[package]]
 name = "idna"
-version = "3.8"
+version = "3.10"
 requires_python = ">=3.6"
 summary = "Internationalized Domain Names in Applications (IDNA)"
 groups = ["default"]
 files = [
-    {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"},
-    {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
+    {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
+    {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
+]
+
+[[package]]
+name = "invoke"
+version = "2.2.0"
+requires_python = ">=3.6"
+summary = "Pythonic task execution"
+groups = ["default"]
+files = [
+    {file = "invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820"},
+    {file = "invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5"},
 ]
 
 [[package]]
 name = "jinja2"
-version = "3.1.4"
+version = "3.1.5"
 requires_python = ">=3.7"
 summary = "A very fast and expressive template engine."
 groups = ["default"]
@@ -306,65 +338,85 @@
     "MarkupSafe>=2.0",
 ]
 files = [
-    {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
-    {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
+    {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
+    {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
 ]
 
 [[package]]
 name = "markupsafe"
-version = "2.1.5"
-requires_python = ">=3.7"
+version = "3.0.2"
+requires_python = ">=3.9"
 summary = "Safely add untrusted strings to HTML/XML markup."
 groups = ["default"]
 files = [
-    {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
-    {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
-    {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
-    {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
-    {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
-    {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
-    {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
-    {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
-    {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
-    {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
-    {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
-    {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
-    {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
-    {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
-    {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
-    {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
-    {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
-    {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
-    {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
-    {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
-    {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"},
+    {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"},
+    {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"},
+    {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"},
+    {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
 ]
 
 [[package]]
 name = "more-itertools"
-version = "10.4.0"
-requires_python = ">=3.8"
+version = "10.6.0"
+requires_python = ">=3.9"
 summary = "More routines for operating on iterables, beyond itertools"
 groups = ["default"]
 files = [
-    {file = "more-itertools-10.4.0.tar.gz", hash = "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923"},
-    {file = "more_itertools-10.4.0-py3-none-any.whl", hash = "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27"},
+    {file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"},
+    {file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"},
 ]
 
 [[package]]
 name = "packaging"
-version = "24.1"
+version = "24.2"
 requires_python = ">=3.8"
 summary = "Core utilities for Python packages"
 groups = ["default"]
 files = [
-    {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
-    {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
+    {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
+    {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
 ]
 
 [[package]]
 name = "paramiko"
-version = "3.4.1"
+version = "3.5.0"
 requires_python = ">=3.6"
 summary = "SSH2 protocol library"
 groups = ["default"]
@@ -374,25 +426,25 @@
     "pynacl>=1.5",
 ]
 files = [
-    {file = "paramiko-3.4.1-py3-none-any.whl", hash = "sha256:8e49fd2f82f84acf7ffd57c64311aa2b30e575370dc23bdb375b10262f7eac32"},
-    {file = "paramiko-3.4.1.tar.gz", hash = "sha256:8b15302870af7f6652f2e038975c1d2973f06046cb5d7d65355668b3ecbece0c"},
+    {file = "paramiko-3.5.0-py3-none-any.whl", hash = "sha256:1fedf06b085359051cd7d0d270cebe19e755a8a921cc2ddbfa647fb0cd7d68f9"},
+    {file = "paramiko-3.5.0.tar.gz", hash = "sha256:ad11e540da4f55cedda52931f1a3f812a8238a7af7f62a60de538cd80bb28124"},
 ]
 
 [[package]]
 name = "psutil"
-version = "6.0.0"
+version = "6.1.1"
 requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
 summary = "Cross-platform lib for process and system monitoring in Python."
 groups = ["default"]
 files = [
-    {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"},
-    {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"},
-    {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"},
-    {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"},
-    {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"},
-    {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"},
-    {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"},
-    {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"},
+    {file = "psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8"},
+    {file = "psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377"},
+    {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003"},
+    {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160"},
+    {file = "psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3"},
+    {file = "psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53"},
+    {file = "psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649"},
+    {file = "psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5"},
 ]
 
 [[package]]
@@ -408,13 +460,12 @@
 
 [[package]]
 name = "pyinfra"
-version = "3.1"
+version = "3.2"
 requires_python = ">=3.8"
 summary = "pyinfra automates/provisions/manages/deploys infrastructure."
 groups = ["default"]
 dependencies = [
     "click>2",
-    "configparser",
     "distro<2,>=1.6",
     "gevent>=1.5",
     "graphlib-backport; python_version < \"3.9\"",
@@ -429,8 +480,8 @@
     "typing-extensions; python_version < \"3.11\"",
 ]
 files = [
-    {file = "pyinfra-3.1-py2.py3-none-any.whl", hash = "sha256:d3f52d61c9d9bea23175af250299ef60c2d6ebad86f771e3b546dcd96e0dd060"},
-    {file = "pyinfra-3.1.tar.gz", hash = "sha256:96fca7c54201ffe00e452b165662423f27cd2a3ae6c9f0272fb04c61c078dc06"},
+    {file = "pyinfra-3.2-py2.py3-none-any.whl", hash = "sha256:ca8e6f4b51031b3b56b4ab6e7ce4cb749530ec92d6ba22f1f2657e2d20231416"},
+    {file = "pyinfra-3.2.tar.gz", hash = "sha256:ea65f2048860dae6a7d86470d4a7ca67c9e2e82d671e781b849f1e5e163ad6db"},
 ]
 
 [[package]]
@@ -456,8 +507,19 @@
 ]
 
 [[package]]
+name = "pyparsing"
+version = "3.2.1"
+requires_python = ">=3.9"
+summary = "pyparsing module - Classes and methods to define and execute parsing grammars"
+groups = ["default"]
+files = [
+    {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"},
+    {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"},
+]
+
+[[package]]
 name = "pyspnego"
-version = "0.11.1"
+version = "0.11.2"
 requires_python = ">=3.8"
 summary = "Windows Negotiate Authentication Client and Server"
 groups = ["default"]
@@ -466,8 +528,8 @@
     "sspilib>=0.1.0; sys_platform == \"win32\"",
 ]
 files = [
-    {file = "pyspnego-0.11.1-py3-none-any.whl", hash = "sha256:129a4294f2c4d681d5875240ef87accc6f1d921e8983737fb0b59642b397951e"},
-    {file = "pyspnego-0.11.1.tar.gz", hash = "sha256:e92ed8b0a62765b9d6abbb86a48cf871228ddb97678598dc01c9c39a626823f6"},
+    {file = "pyspnego-0.11.2-py3-none-any.whl", hash = "sha256:74abc1fb51e59360eb5c5c9086e5962174f1072c7a50cf6da0bda9a4bcfdfbd4"},
+    {file = "pyspnego-0.11.2.tar.gz", hash = "sha256:994388d308fb06e4498365ce78d222bf4f3570b6df4ec95738431f61510c971b"},
 ]
 
 [[package]]
@@ -501,6 +563,21 @@
 ]
 
 [[package]]
+name = "rdflib"
+version = "7.1.3"
+requires_python = "<4.0.0,>=3.8.1"
+summary = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information."
+groups = ["default"]
+dependencies = [
+    "isodate<1.0.0,>=0.7.2; python_version < \"3.11\"",
+    "pyparsing<4,>=2.1.0",
+]
+files = [
+    {file = "rdflib-7.1.3-py3-none-any.whl", hash = "sha256:5402310a9f0f3c07d453d73fd0ad6ba35616286fe95d3670db2b725f3f539673"},
+    {file = "rdflib-7.1.3.tar.gz", hash = "sha256:f3dcb4c106a8cd9e060d92f43d593d09ebc3d07adc244f4c7315856a12e383ee"},
+]
+
+[[package]]
 name = "requests"
 version = "2.32.3"
 requires_python = ">=3.8"
@@ -535,53 +612,59 @@
 
 [[package]]
 name = "setuptools"
-version = "73.0.1"
-requires_python = ">=3.8"
+version = "75.8.0"
+requires_python = ">=3.9"
 summary = "Easily download, build, install, upgrade, and uninstall Python packages"
 groups = ["default"]
 files = [
-    {file = "setuptools-73.0.1-py3-none-any.whl", hash = "sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e"},
-    {file = "setuptools-73.0.1.tar.gz", hash = "sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193"},
+    {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"},
+    {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"},
 ]
 
 [[package]]
 name = "six"
-version = "1.16.0"
-requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+version = "1.17.0"
+requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 summary = "Python 2 and 3 compatibility utilities"
 groups = ["default"]
 files = [
-    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
-    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+    {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
+    {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
 ]
 
 [[package]]
 name = "sspilib"
-version = "0.1.0"
+version = "0.2.0"
 requires_python = ">=3.8"
 summary = "SSPI API bindings for Python"
 groups = ["default"]
 marker = "sys_platform == \"win32\""
 files = [
-    {file = "sspilib-0.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09d7f72ad5e4bbf9a8f1acf0d5f0c3f9fbe500f44c4a45ac24a99ece84f5654f"},
-    {file = "sspilib-0.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e5705e11aaa030a61d2b0a2ce09d2b8a1962dd950e55adc7a3c87dd463c6878"},
-    {file = "sspilib-0.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dced8213d311c56f5f38044716ebff5412cc156f19678659e8ffa9bb6a642bd7"},
-    {file = "sspilib-0.1.0-cp311-cp311-win32.whl", hash = "sha256:d30d38d52dbd857732224e86ae3627d003cc510451083c69fa481fc7de88a7b6"},
-    {file = "sspilib-0.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:61c9067168cce962f7fead42c28804c3a39a164b9a7b660200b8cfe31e3af071"},
-    {file = "sspilib-0.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:b526b8e5a236553f5137b951b89a2f108f56138ad05f31fd0a51b10f80b6c3cc"},
-    {file = "sspilib-0.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3ff356d40cd34c900f94f1591eaabd458284042af611ebc1dbf609002066dba5"},
-    {file = "sspilib-0.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b0fee3a52d0acef090f6c9b49953a8400fdc1c10aca7334319414a3038aa493"},
-    {file = "sspilib-0.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab52d190dad1d578ec40d1fb417a8571954f4e32f35442a14cb709f57d3acbc9"},
-    {file = "sspilib-0.1.0-cp312-cp312-win32.whl", hash = "sha256:b3cf819094383ec883e9a63c11b81d622618c815c18a6c9d761d9a14d9f028d1"},
-    {file = "sspilib-0.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:b83825a2c43ff84ddff72d09b098057efaabf3841d3c42888078e154cf8e9595"},
-    {file = "sspilib-0.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:9aa6ab4c3fc1057251cf1f3f199daf90b99599cdfafc9eade8fdf0c01526dec8"},
-    {file = "sspilib-0.1.0.tar.gz", hash = "sha256:58b5291553cf6220549c0f855e0e6973f4977375d8236ce47bb581efb3e9b1cf"},
+    {file = "sspilib-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e0943204c8ba732966fdc5b69e33cf61d8dc6b24e6ed875f32055d9d7e2f76cd"},
+    {file = "sspilib-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1cdfc5ec2f151f26e21aa50ccc7f9848c969d6f78264ae4f38347609f6722df"},
+    {file = "sspilib-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6c33495a3de1552120c4a99219ebdd70e3849717867b8cae3a6a2f98fef405"},
+    {file = "sspilib-0.2.0-cp311-cp311-win32.whl", hash = "sha256:400d5922c2c2261009921157c4b43d868e84640ad86e4dc84c95b07e5cc38ac6"},
+    {file = "sspilib-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3e7d19c16ba9189ef8687b591503db06cfb9c5eb32ab1ca3bb9ebc1a8a5f35c"},
+    {file = "sspilib-0.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:f65c52ead8ce95eb78a79306fe4269ee572ef3e4dcc108d250d5933da2455ecc"},
+    {file = "sspilib-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:abac93a90335590b49ef1fc162b538576249c7f58aec0c7bcfb4b860513979b4"},
+    {file = "sspilib-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1208720d8e431af674c5645cec365224d035f241444d5faa15dc74023ece1277"},
+    {file = "sspilib-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48dceb871ecf9cf83abdd0e6db5326e885e574f1897f6ae87d736ff558f4bfa"},
+    {file = "sspilib-0.2.0-cp312-cp312-win32.whl", hash = "sha256:bdf9a4f424add02951e1f01f47441d2e69a9910471e99c2c88660bd8e184d7f8"},
+    {file = "sspilib-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:40a97ca83e503a175d1dc9461836994e47e8b9bcf56cab81a2c22e27f1993079"},
+    {file = "sspilib-0.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8ffc09819a37005c66a580ff44f544775f9745d5ed1ceeb37df4e5ff128adf36"},
+    {file = "sspilib-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:40ff410b64198cf1d704718754fc5fe7b9609e0c49bf85c970f64c6fc2786db4"},
+    {file = "sspilib-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:02d8e0b6033de8ccf509ba44fdcda7e196cdedc0f8cf19eb22c5e4117187c82f"},
+    {file = "sspilib-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7943fe14f8f6d72623ab6401991aa39a2b597bdb25e531741b37932402480f"},
+    {file = "sspilib-0.2.0-cp313-cp313-win32.whl", hash = "sha256:b9044d6020aa88d512e7557694fe734a243801f9a6874e1c214451eebe493d92"},
+    {file = "sspilib-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c39a698491f43618efca8776a40fb7201d08c415c507f899f0df5ada15abefaa"},
+    {file = "sspilib-0.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:863b7b214517b09367511c0ef931370f0386ed2c7c5613092bf9b106114c4a0e"},
+    {file = "sspilib-0.2.0.tar.gz", hash = "sha256:4d6cd4290ca82f40705efeb5e9107f7abcd5e647cb201a3d04371305938615b8"},
 ]
 
 [[package]]
 name = "typeguard"
-version = "4.3.0"
-requires_python = ">=3.8"
+version = "4.4.1"
+requires_python = ">=3.9"
 summary = "Run-time type checker for Python"
 groups = ["default"]
 dependencies = [
@@ -589,8 +672,8 @@
     "typing-extensions>=4.10.0",
 ]
 files = [
-    {file = "typeguard-4.3.0-py3-none-any.whl", hash = "sha256:4d24c5b39a117f8a895b9da7a9b3114f04eb63bade45a4492de49b175b6f7dfa"},
-    {file = "typeguard-4.3.0.tar.gz", hash = "sha256:92ee6a0aec9135181eae6067ebd617fd9de8d75d714fb548728a4933b1dea651"},
+    {file = "typeguard-4.4.1-py3-none-any.whl", hash = "sha256:9324ec07a27ec67fc54a9c063020ca4c0ae6abad5e9f0f9804ca59aee68c6e21"},
+    {file = "typeguard-4.4.1.tar.gz", hash = "sha256:0d22a89d00b453b47c49875f42b6601b961757541a2e1e0ef517b6e24213c21b"},
 ]
 
 [[package]]
@@ -606,24 +689,24 @@
 
 [[package]]
 name = "urllib3"
-version = "2.2.2"
-requires_python = ">=3.8"
+version = "2.3.0"
+requires_python = ">=3.9"
 summary = "HTTP library with thread-safe connection pooling, file post, and more."
 groups = ["default"]
 files = [
-    {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
-    {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
+    {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"},
+    {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"},
 ]
 
 [[package]]
 name = "xmltodict"
-version = "0.13.0"
-requires_python = ">=3.4"
+version = "0.14.2"
+requires_python = ">=3.6"
 summary = "Makes working with XML feel like you are working with JSON"
 groups = ["default"]
 files = [
-    {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"},
-    {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"},
+    {file = "xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac"},
+    {file = "xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553"},
 ]
 
 [[package]]
@@ -642,7 +725,7 @@
 
 [[package]]
 name = "zope-interface"
-version = "7.0.1"
+version = "7.2"
 requires_python = ">=3.8"
 summary = "Interfaces for Python"
 groups = ["default"]
@@ -650,20 +733,23 @@
     "setuptools",
 ]
 files = [
-    {file = "zope.interface-7.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03bd5c0db82237bbc47833a8b25f1cc090646e212f86b601903d79d7e6b37031"},
-    {file = "zope.interface-7.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f52050c6a10d4a039ec6f2c58e5b3ade5cc570d16cf9d102711e6b8413c90e6"},
-    {file = "zope.interface-7.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af0b33f04677b57843d529b9257a475d2865403300b48c67654c40abac2f9f24"},
-    {file = "zope.interface-7.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:696c2a381fc7876b3056711717dba5eddd07c2c9e5ccd50da54029a1293b6e43"},
-    {file = "zope.interface-7.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f89a420cf5a6f2aa7849dd59e1ff0e477f562d97cf8d6a1ee03461e1eec39887"},
-    {file = "zope.interface-7.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:b59deb0ddc7b431e41d720c00f99d68b52cb9bd1d5605a085dc18f502fe9c47f"},
-    {file = "zope.interface-7.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52f5253cca1b35eaeefa51abd366b87f48f8714097c99b131ba61f3fdbbb58e7"},
-    {file = "zope.interface-7.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88d108d004e0df25224de77ce349a7e73494ea2cb194031f7c9687e68a88ec9b"},
-    {file = "zope.interface-7.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c203d82069ba31e1f3bc7ba530b2461ec86366cd4bfc9b95ec6ce58b1b559c34"},
-    {file = "zope.interface-7.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3495462bc0438b76536a0e10d765b168ae636092082531b88340dc40dcd118"},
-    {file = "zope.interface-7.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192b7a792e3145ed880ff6b1a206fdb783697cfdb4915083bfca7065ec845e60"},
-    {file = "zope.interface-7.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:400d06c9ec8dbcc96f56e79376297e7be07a315605c9a2208720da263d44d76f"},
-    {file = "zope.interface-7.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c1dff87b30fd150c61367d0e2cdc49bb55f8b9fd2a303560bbc24b951573ae1"},
-    {file = "zope.interface-7.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f749ca804648d00eda62fe1098f229b082dfca930d8bad8386e572a6eafa7525"},
-    {file = "zope.interface-7.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ec212037becf6d2f705b7ed4538d56980b1e7bba237df0d8995cbbed29961dc"},
-    {file = "zope.interface-7.0.1.tar.gz", hash = "sha256:f0f5fda7cbf890371a59ab1d06512da4f2c89a6ea194e595808123c863c38eff"},
+    {file = "zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2"},
+    {file = "zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22"},
+    {file = "zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7"},
+    {file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c"},
+    {file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a"},
+    {file = "zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1"},
+    {file = "zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7"},
+    {file = "zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465"},
+    {file = "zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89"},
+    {file = "zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54"},
+    {file = "zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d"},
+    {file = "zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5"},
+    {file = "zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98"},
+    {file = "zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d"},
+    {file = "zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c"},
+    {file = "zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398"},
+    {file = "zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b"},
+    {file = "zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd"},
+    {file = "zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe"},
 ]
--- a/pyproject.toml	Mon Jan 20 14:10:19 2025 -0800
+++ b/pyproject.toml	Mon Jan 20 21:55:08 2025 -0800
@@ -6,9 +6,11 @@
     {name = "", email = ""},
 ]
 dependencies = [
-    "pyinfra>=3.1",
+    "pyinfra>=3.2",
     "more-itertools>=10.2.0",
     "psutil>=5.9.8",
+    "rdflib>=7.1.3",
+    "invoke>=2.2.0",
 ]
 requires-python = ">=3.11"
 license = {text = "MIT"}
@@ -17,4 +19,3 @@
 requires = ["pdm-pep517>=1.0.0"]
 build-backend = "pdm.pep517.api"
 
-[tool.pdm]
--- a/ssh.py	Mon Jan 20 14:10:19 2025 -0800
+++ b/ssh.py	Mon Jan 20 21:55:08 2025 -0800
@@ -1,17 +1,3 @@
-from pyinfra import host
+from pyinfra.context import host
 from pyinfra.facts.server import LinuxDistribution
 from pyinfra.operations import files, systemd
-
-
-systemd.service(
-    service='ssh',
-    running=True,
-    enabled=True,
-)
-
-files.line(path='/etc/ssh/ssh_config', line="HashKnownHosts", replace="HashKnownHosts no")
-
-if 'pi' not in host.groups:
-    files.line(path='/etc/ssh/sshd_config', line="^UseDNS\b", replace="UseDNS no")
-    # MAYBE plus needs this fix: adding ListenAddress 0.0.0.0 to /etc/ssh/sshd_config
-    systemd.service(service='sshd', reloaded=True)
--- a/sync.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-from pathlib import Path
-
-from pyinfra import host
-from pyinfra.facts.server import Command
-from pyinfra.operations import apt, files, server, systemd
-
-
-def host_running_version(user: str, v: str):
-    # if this command fails, we go in to error state and run nothing else #bugreport
-    out = host.get_fact(Command, f"runuser -u {user} syncthing serve --version || exit 0")
-    if out is None:
-        return False
-    _, sv, *_ = out.split()
-    return sv == v
-
-
-def install_syncthing(user, version, os='linux', arch='amd64'):
-    tmpdir = Path('/tmp/syncthing_install')
-    dl_name = f'syncthing-{os}-{arch}-{version}'
-    url = f'https://github.com/syncthing/syncthing/releases/download/{version}/{dl_name}.tar.gz'
-    files.directory(path=tmpdir)
-    files.download(src=url, dest=str(tmpdir / f'{dl_name}.tgz'))  # bugreport
-    server.shell(commands=[f'cd {tmpdir}; aunpack {dl_name}.tgz'])
-
-    systemd.service(service=f'syncthing@{user}', running=False)
-
-    user_svc_template = '/lib/systemd/system/syncthing@.service'
-    server.shell(commands=[
-        f'cp -a {tmpdir}/{dl_name}/{s} {d}' for s, d in [
-            ('syncthing', '/usr/bin'),
-            ('etc/linux-systemd/system/syncthing@.service', user_svc_template),
-        ]
-    ])
-    files.link(path=f'/etc/systemd/system/multi-user.target.wants/syncthing@{user}.service', target=user_svc_template)
-    systemd.service(service=f'syncthing@{user}', enabled=True, restarted=True, daemon_reload=True)
-
-
-# also see /my/serv/filesync/syncthing/deploy.yaml for the container one
-version = 'v1.27.10'
-
-# primary instance is in k8s (/my/serv/filesync/syncthing); the rest are run with systemd.
-# Configs are in ~/.config/syncthing/ on each box
-if host.data.get('syncthing'):
-    apt.packages(packages=['syncthing'], present=False)
-    user = 'ari' if host.name == 'dot' else 'drewp'
-
-    if not host_running_version(user, version):
-        install_syncthing(user, version)
-
-# something above has broken devnull #bugreport
-server.shell(commands=['chmod a+w /dev/null'])
-
-# also consider https://github.com/Martchus/syncthingtray tray status viewer on dtops
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sync/sync.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,57 @@
+from pathlib import Path
+
+from pyinfra.context import host
+from pyinfra.facts.server import Command
+from pyinfra.operations import apt, files, server, systemd
+
+
+def host_running_version(user: str, v: str):
+    # if this command fails, we go in to error state and run nothing else #bugreport
+    out = host.get_fact(Command, f"runuser -u {user} syncthing serve --version || exit 0")
+    if out is None:
+        return False
+    _, sv, *_ = out.split()
+    return sv == v
+
+
+def install_syncthing(user, version, os='linux', arch='amd64'):
+    tmpdir = Path('/tmp/syncthing_install')
+    dl_name = f'syncthing-{os}-{arch}-{version}'
+    url = f'https://github.com/syncthing/syncthing/releases/download/{version}/{dl_name}.tar.gz'
+    files.directory(path=tmpdir)
+    files.download(src=url, dest=str(tmpdir / f'{dl_name}.tgz'))  # bugreport
+    server.shell(commands=[f'cd {tmpdir}; aunpack {dl_name}.tgz'])
+
+    systemd.service(service=f'syncthing@{user}', running=False)
+
+    user_svc_template = '/lib/systemd/system/syncthing@.service'
+    server.shell(commands=[
+        f'cp -a {tmpdir}/{dl_name}/{s} {d}' for s, d in [
+            ('syncthing', '/usr/bin'),
+            ('etc/linux-systemd/system/syncthing@.service', user_svc_template),
+        ]
+    ])
+    files.link(path=f'/etc/systemd/system/multi-user.target.wants/syncthing@{user}.service', target=user_svc_template)
+    systemd.service(service=f'syncthing@{user}', enabled=True, restarted=True, daemon_reload=True)
+
+def syncthing():
+    # also see /my/serv/filesync/syncthing/deploy.yaml for the container one
+    version = 'v1.27.10'
+
+    # primary instance is in k8s (/my/serv/filesync/syncthing); the rest are run with systemd.
+    # Configs are in ~/.config/syncthing/ on each box
+    if host.data.get('syncthing'):
+        apt.packages(packages=['syncthing'], present=False)
+        user = 'ari' if host.name == 'dot' else 'drewp'
+
+        if not host_running_version(user, version):
+            install_syncthing(user, version)
+
+    # something above has broken devnull #bugreport
+    server.shell(commands=['chmod a+w /dev/null'])
+
+    # also consider https://github.com/Martchus/syncthingtray tray status viewer on dtops
+
+operations = [
+    syncthing,
+]
\ No newline at end of file
--- a/system.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-import os
-from io import StringIO
-from typing import cast
-
-import pyinfra
-from pyinfra import host
-from pyinfra.operations import apt, files, server, systemd
-
-TZ = 'America/Los_Angeles'
-
-
-def timezone():
-    files.link(path='/etc/localtime', target=f'/usr/share/zoneinfo/{TZ}')
-    files.replace(path='/etc/timezone', text='.*', replace=TZ)
-
-
-def fstab():
-    fstab_file = f'files/fstab/{host.name}'
-    if os.path.exists(fstab_file):
-        files.put(src=fstab_file, dest='/etc/fstab')
-
-
-def pi_tmpfs():
-    for line in [
-            'tmpfs /var/log tmpfs defaults,noatime,mode=0755 0 0',
-            'tmpfs /tmp tmpfs defaults,noatime 0 0',
-    ]:
-        files.line(path="/etc/fstab", line=line, replace=line)
-
-    # stop SD card corruption (along with some mounts in fstab)
-    apt.packages(packages=['dphys-swapfile'], present=False)
-
-
-def no_sleep():
-    server.shell(commands=['systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target'])
-
-
-def nfs_server():
-    # remove when we're on longhorn
-    apt.packages(packages=['nfs-kernel-server'])
-    files.template(src='templates/bang_exports.j2', dest='/etc/exports')
-
-
-def smaller_journals():
-    files.line(name='shorter systemctl log window, for disk space',
-               path='/etc/systemd/journald.conf',
-               line='MaxFileSec',
-               replace="MaxFileSec=7day")
-
-
-def web_forward():
-    for port in [80, 443]:
-        svc = f'web_forward_{port}'
-        files.template(src="templates/webforward.service.j2",
-                       dest=f"/etc/systemd/system/{svc}.service",
-                       serv_host='bang',
-                       port=port,
-                       name='web',
-                       fam='tcp')
-        systemd.service(service=svc, enabled=True, restarted=True)
-
-
-def minecraft_forward():
-    port = 25765
-    for fam in ['tcp', 'udp']:
-        svc = f'mc_smp_{fam}_forward_{port}'
-        files.template(src="templates/webforward.service.j2",
-                       dest=f"/etc/systemd/system/{svc}.service",
-                       serv_host='ditto',
-                       port=port,
-                       name='mc_smp',
-                       fam=fam)
-        systemd.service(service=svc, enabled=True, restarted=True)
-
-
-def pigpiod():
-    files.put(src="files/pigpiod.service", dest="/etc/systemd/system/pigpiod.service")
-    systemd.service(service='pigpiod', daemon_reload=True, enabled=True)
-
-
-def rpi_iscsi_volumes():
-    iscsi_dir = '/d2/rpi-iscsi'
-    for pi_hostname in cast(list, pyinfra.inventory.get_group(name='pi')):
-        out = f'{iscsi_dir}/{pi_hostname}.disk'
-        files.directory(path=iscsi_dir)
-        server.shell(commands=f'dd if=/dev/zero of={out} count=0 bs=1 seek=10G conv=excl || true')
-        files.put(dest=f"/etc/tgt/conf.d/{pi_hostname}.conf",
-                  src=StringIO(f"""
-<target iqn.2024-03.com.bigasterisk:{pi_hostname}.target>
-    backing-store {out}
-    initiator-name iqn.2024-03.com.bigasterisk:{pi_hostname}.initiator
-</target> 
-                            """))
-    # restarting is disruptive to connected pis, and they might need to be
-    # visited:
-    #systemd.service(service='tgt.service', running=True, restarted=True)
-
-
-server.hostname(hostname=host.name)
-timezone()
-fstab()
-
-if host.name == 'ditto':
-    rpi_iscsi_volumes()
-
-if 'pi' not in host.groups:
-    files.line(path='/etc/update-manager/release-upgrades', line="^Prompt=", replace="Prompt=normal")
-
-if 'pi' in host.groups:
-    pi_tmpfs()
-
-if host.name in ['bang', 'pipe', 'ditto']:
-    no_sleep()
-
-if host.name in ['bang', 'ditto']:
-    nfs_server()
-
-if host.name in ['prime', 'ditto', 'pipe']:
-    smaller_journals()
-
-if host.name == 'prime':
-    web_forward()
-    minecraft_forward()
-
-if 'pi' in host.groups:
-    pigpiod()
-
-# for space, consider:
-# k3s crictl rmi --prune
-# snap list --all | while read snapname ver rev trk pub notes; do if [[ $notes = *disabled* ]]; then snap remove "$snapname" --revision="$rev"; fi; done
-# podman system reset
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/files/ditto_exports	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,7 @@
+# written by pyinfra
+
+# zfs takes care of its own
+
+# photoprism on ditto
+# /d4/photoprism    10.5.0.7(rw,no_root_squash)
+# /d4/frigate       10.5.0.7(rw,no_root_squash)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/files/pigpiod.service	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,11 @@
+# written by pyinfra
+
+[Unit]
+Description=Daemon required to control GPIO pins via pigpio
+
+[Service]
+Type=simple
+ExecStart=/usr/bin/pigpiod -g -p 8888
+
+[Install]
+WantedBy=multi-user.target
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/fstabs/bang	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,8 @@
+# written by pyinfra
+
+# <file system> <mount point>   <type>  <options>       <dump>  <pass>
+/dev/disk/by-uuid/8c7a2d08-60d1-486a-8136-d9f43d83a064 / ext4 relatime 0 0
+/dev/disk/by-uuid/d9a1e1e4-9eba-4988-8b01-c5f6732a2972 /d3 ext4 noatime 0 0
+/dev/disk/by-partuuid/77687eec-15bf-9345-b420-bb83659e6a6b /d4 ext4 noatime 0 0
+
+ditto5:/my                           /my       nfs  rw,noatime          0       0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/fstabs/dash	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,11 @@
+# written by pyinfra
+
+# <file system> <mount point>   <type>  <options>       <dump>  <pass>
+/dev/disk/by-uuid/d8d23ff1-7c37-4a7d-9fc4-55fc61f912a0 / ext4 defaults 0 1
+/dev/disk/by-uuid/CB55-821E /boot/efi vfat defaults 0 1
+
+UUID=73bcd201-5f77-4f68-9fba-47835c3c1692 /d2  ext4  defaults 0 0
+UUID=6cae1c30-3c91-4aa7-9e9f-fcbd7ff706fe /d3  ext4  defaults 0 0
+UUID=3b6780e0-ec86-43be-8d09-e462dbad762e /d4  ext4  defaults 0 0
+
+ditto5:/my                                /my  nfs   rw,noatime 0 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/fstabs/ditto	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,14 @@
+# written by pyinfra
+
+
+#
+# Use 'blkid' to print the universally unique identifier for a
+# device; this may be used with UUID= as a more robust way to name devices
+# that works even if disks are added and removed. See fstab(5).
+#
+# <file system> <mount point>   <type>  <options>       <dump>  <pass>
+/dev/disk/by-uuid/6e64ce62-34db-4084-9385-d001e99ad38b / ext4 defaults 0 1
+/dev/disk/by-uuid/3F95-42F4 /boot/efi vfat defaults 0 1
+/dev/disk/by-uuid/74795c77-ed20-417d-988a-abc09c3dfc27 /d2 ext4 defaults 0 1
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/fstabs/dot	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,10 @@
+# written by pyinfra
+
+# <file system> <mount point>   <type>  <options>       <dump>  <pass>
+
+
+
+/dev/disk/by-uuid/a9403f0b-aa16-4096-ab0d-2e2069d3f18a / ext4 defaults 0 1
+
+/dev/mapper/ubuntu--vg-ubuntu--lv                      /d2 ext4 defaults 0 1
+/dev/disk/by-uuid/5a6ce8db-cde0-4c26-b6a4-08faef2e01a2 /d3 ext4 defaults 0 1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/fstabs/slash	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,7 @@
+# written by pyinfra
+
+# <file system> <mount point>   <type>  <options>       <dump>  <pass>
+UUID=df079890-9431-4e17-940c-d9ed8ce4e149 /         ext4 errors=remount-ro 0       1
+UUID=1CFA-995B                            /boot/efi vfat umask=0077        0       1
+
+ditto5:/my                                /my       nfs  rw,noatime        0       0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/fstabs/tofu	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,4 @@
+# written by pyinfra
+
+# <file system> <mount point>   <type>  <options>       <dump>  <pass>
+/dev/nvme0n1p6 / ext4 rw,relatime 0 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/system.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,155 @@
+import os
+from io import StringIO
+from typing import cast
+
+import pyinfra
+from pyinfra.context import host
+from pyinfra.operations import apt, files, server, systemd
+
+TZ = 'America/Los_Angeles'
+
+
+def sshServer():
+    systemd.service(
+        service='ssh',
+        running=True,
+        enabled=True,
+    )
+
+    files.line(path='/etc/ssh/ssh_config', line="HashKnownHosts", replace="HashKnownHosts no")
+
+    if 'pi' not in host.groups:
+        files.line(path='/etc/ssh/sshd_config', line="^UseDNS\b", replace="UseDNS no")
+        # MAYBE plus needs this fix: adding ListenAddress 0.0.0.0 to /etc/ssh/sshd_config
+        systemd.service(service='sshd', reloaded=True)
+
+
+def timezone():
+    files.link(path='/etc/localtime', target=f'/usr/share/zoneinfo/{TZ}')
+    files.replace(path='/etc/timezone', text='.*', replace=TZ)
+
+
+def fstab():
+    fstab_file = f'system/fstabs/{host.name}'
+    if os.path.exists(fstab_file):
+        files.put(src=fstab_file, dest='/etc/fstab')
+
+
+def pi_tmpfs():
+    if 'pi' not in host.groups:
+        return
+
+    for line in [
+            'tmpfs /var/log tmpfs defaults,noatime,mode=0755 0 0',
+            'tmpfs /tmp tmpfs defaults,noatime 0 0',
+    ]:
+        files.line(path="/etc/fstab", line=line, replace=line)
+
+    # stop SD card corruption (along with some mounts in fstab)
+    apt.packages(packages=['dphys-swapfile'], present=False)
+
+
+def no_sleep():
+    if host.name not in ['bang', 'pipe', 'ditto']:
+        return
+
+    server.shell(commands=['systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target'])
+
+
+def nfs_server():
+    if host.name != 'ditto':
+        return
+
+    # remove when we're on longhorn
+    apt.packages(packages=['nfs-kernel-server'])
+    files.put(src='system/files/ditto_exports', dest='/etc/exports')
+
+
+def smaller_journals():
+    if host.name not in ['prime', 'ditto', 'pipe']:
+        return
+    files.line(name='shorter systemctl log window, for disk space',
+               path='/etc/systemd/journald.conf',
+               line='MaxFileSec',
+               replace="MaxFileSec=7day")
+
+
+def web_forward():
+    if host.name != 'prime':
+        return
+    for port in [80, 443]:
+        svc = f'web_forward_{port}'
+        files.template(src="system/templates/webforward.service.j2",
+                       dest=f"/etc/systemd/system/{svc}.service",
+                       serv_host='bang',
+                       port=port,
+                       name='web',
+                       fam='tcp')
+        systemd.service(service=svc, enabled=True, restarted=True)
+
+
+def minecraft_forward():
+    if host.name != 'prime':
+        return
+    port = 25765
+    for fam in ['tcp', 'udp']:
+        svc = f'mc_smp_{fam}_forward_{port}'
+        files.template(src="system/templates/webforward.service.j2",
+                       dest=f"/etc/systemd/system/{svc}.service",
+                       serv_host='ditto',
+                       port=port,
+                       name='mc_smp',
+                       fam=fam)
+        systemd.service(service=svc, enabled=True, restarted=True)
+
+
+def pigpiod():
+    if 'pi' not in host.groups:
+        return
+    files.put(src="system/files/pigpiod.service", dest="/etc/systemd/system/pigpiod.service")
+    systemd.service(service='pigpiod', daemon_reload=True, enabled=True)
+
+
+def rpi_iscsi_volumes():
+    if host.name != 'ditto':
+        return
+
+    iscsi_dir = '/d2/rpi-iscsi'
+    for pi_hostname in cast(list, pyinfra.inventory.get_group(name='pi')):
+        out = f'{iscsi_dir}/{pi_hostname}.disk'
+        files.directory(path=iscsi_dir)
+        server.shell(commands=f'dd if=/dev/zero of={out} count=0 bs=1 seek=10G conv=excl || true')
+        files.put(dest=f"/etc/tgt/conf.d/{pi_hostname}.conf",
+                  src=StringIO(f"""
+<target iqn.2024-03.com.bigasterisk:{pi_hostname}.target>
+    backing-store {out}
+    initiator-name iqn.2024-03.com.bigasterisk:{pi_hostname}.initiator
+</target> 
+                            """))
+    # restarting is disruptive to connected pis, and they might need to be
+    # visited:
+    #systemd.service(service='tgt.service', running=True, restarted=True)
+
+
+def hostname():
+    server.hostname(hostname=host.name)
+
+
+
+operations = [
+    hostname,
+    timezone,
+    fstab,
+    rpi_iscsi_volumes,
+    pi_tmpfs,
+    no_sleep,
+    nfs_server,
+    smaller_journals,
+    web_forward,
+    minecraft_forward,
+    pigpiod,
+]
+# for space, consider:
+# k3s crictl rmi --prune
+# snap list --all | while read snapname ver rev trk pub notes; do if [[ $notes = *disabled* ]]; then snap remove "$snapname" --revision="$rev"; fi; done
+# podman system reset
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/system/templates/webforward.service.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,16 @@
+# written by pyinfra
+
+[Unit]
+Description={{ fam }} forward for port {{ port }}
+Requires=network.target
+Wants=nss-lookup.target
+Before=nss-lookup.target
+After=network.target
+
+[Service]
+Type=simple
+
+ExecStart=/usr/bin/socat {{ 'tcp' if fam=='tcp' else 'udp4' }}-listen:{{ port }},fork,reuseaddr {{ 'tcp' if fam=='tcp' else 'udp' }}:{{serv_host}}:{{ port }}
+
+[Install]
+WantedBy=multi-user.target
--- a/tasks.py	Mon Jan 20 14:10:19 2025 -0800
+++ b/tasks.py	Mon Jan 20 21:55:08 2025 -0800
@@ -1,4 +1,4 @@
-from invoke import task
+from invoke.tasks import task
 
 cmd = '''
 HOME=/root
@@ -13,41 +13,6 @@
 
 
 @task
-def users(ctx):
-    _run(ctx, 'users.py')
-
-
-@task
-def ssh(ctx):
-    _run(ctx, 'ssh.py')
-
-
-@task
-def system(ctx):
-    _run(ctx, 'system.py')
-
-
-@task
-def apt(ctx):
-    _run(ctx, 'apt/apt.py')
-
-
-@task
-def packages(ctx):
-    _run(ctx, 'packages.py')
-
-
-@task
-def net(ctx):
-    _run(ctx, 'net.py')
-
-
-@task
-def dns(ctx):
-    _run(ctx, 'dns.py')
-
-
-@task
 def dns_check(ctx):
     _run(ctx, 'dns_check.py -v')
 
@@ -58,31 +23,6 @@
 
 
 @task
-def wireguard(ctx):
-    _run(ctx, 'wireguard.py')
-
-
-@task
-def kube(ctx):
-    _run(ctx, 'kube.py')
-
-
-@task
-def sync(ctx):
-    _run(ctx, 'sync.py')
-
-
-@task
-def mail(ctx):
-    _run(ctx, 'mail.py')
-
-
-@task
-def home(ctx):
-    _run(ctx, 'home.py')
-
-
-@task
 def multikube(ctx):  # danger- wipes previous k3s
     from multikube_config import server_node, nodes
     ctx.run(cmd + 'inventory.py multikube_wipe.py', pty=True)
@@ -98,21 +38,28 @@
 
 @task
 def all(ctx):
-    configs = [
-        'users.py',
-        'ssh.py',
-        'system.py',
-        'apt/apt.py',
-        'packages.py',
-        'net.py',
-        'dns.py',
-        'wireguard.py',
-        'kube.py',
-        'sync.py',
-        'mail.py',
-        'home.py',
-    ]
-    ctx.run(' '.join([cmd, '--no-wait', '-y', 'inventory.py'] + configs), pty=True)
+    for opGroup in [
+            'users',
+            'system',
+            'apt',
+            'packages',
+            'net',
+            'dns',
+            'wireguard',
+            'kube',
+            'sync',
+            'mail',
+            'home',
+    ]:
+        ctx.run(
+            ' '.join([
+                cmd,  #'--no-wait',
+                '-y',
+                'inventory.py',
+                'all_operations.py'
+            ]),
+            pty=True,
+            env={'GROUP': opGroup})
     ctx.run('touch /my/proj/infra/ran_all.timestamp')
 
 
--- a/templates/bang_exports.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-# written by pyinfra
-
-# zfs takes care of its own
-
-# photoprism on ditto
-/d4/photoprism    10.5.0.7(rw,no_root_squash)
-/d4/frigate       10.5.0.7(rw,no_root_squash)
--- a/templates/dnsmasq/dnsmasq.conf.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-user=nobody
-keep-in-foreground
-log-facility=-
-
-listen-address={{ listen_address }}
-{% if net == "10.2" %}
-# dnsmasq will not automatically listen on the loopback interface. To achieve
-# this, its IP address, 127.0.0.1, must be explicitly given as a
-# --listen-address option.
-listen-address=127.0.0.1
-{% endif %}
-bind-interfaces
-
-domain-needed
-no-resolv
-no-hosts
-addn-hosts=/opt/dnsmasq/{{ net }}/hosts
-local-ttl=30
-mx-host=bigasterisk.com
-cache-size=10000
-neg-ttl=60
-dns-forward-max=1000
-domain=bigasterisk.com
-
-# log-queries
-# log-debug
-
-{% if dhcp_enabled %}
-log-dhcp
-
-dhcp-sequential-ip
-dhcp-broadcast
-dhcp-authoritative
-dhcp-option=option:domain-name,bigasterisk.com
-dhcp-script=/opt/dnsmasq_exporter/on_dhcp_change.sh
-dhcp-hostsfile=/opt/dnsmasq/{{ net }}/dhcp_hosts
-dhcp-leasefile=/opt/dnsmasq/{{ net }}/leases
-dhcp-range={{ house_iface }},10.2.0.0,static,infinite
-dhcp-range=tag:!known,{{ house_iface }},{{ dhcp_range }},2h
-dhcp-option={{ house_iface }},option:dns-server,{{ dns_server }}
-dhcp-option={{ house_iface }},option:router,{{ router }}
-# hosts are tagged in ./dhcp_hosts.j2
-dhcp-option=tag:filtereddns,option:dns-server,10.2.0.4
-
-enable-tftp
-tftp-root=/opt/dnsmasq/tftp
-pxe-service=0,"Raspberry Pi Boot"
- dhcp-mac=set:net-booting-rpi,b8:27:eb:*:*:*
- dhcp-reply-delay=tag:net-booting-rpi,2
-{% endif %}
-
-local=/bigasterisk.com/
-# i didn't say --all-servers, but it was behaving like that
-server=208.201.224.11
-#server=208.201.224.33
-#server=8.8.4.4
-#server=8.8.8.8
-
-{% if net == "10.5" %}
-# net==10.5 is not used for dhcp at all
-# use ./hosts, then try the server that knows the dhcp leases
-server={{ router }}
-{% endif %}
-
-{% if net == '10.2-filtered' %}
-# written by net_routes/dns_blocker.py
-addn-hosts=/opt/dnsmasq/10.2-filtered/dynamic-blocking
-# but! users of this dns server can't even look up names 
-# like 'ditto' since those come from dhcp on the 10.2.0.3
-# (nonfiltered) dnsmasq instance
-{% endif %}
--- a/templates/dnsmasq/dnsmasq.service.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-# written by pyinfra
-
-[Unit]
-Description=dnsmasq for {{ net }} network
-
-# this dnsmasq needs to bind to addr 10.2.0.3
-Requires=network-online.target
-Requires=sys-subsystem-net-devices-eth1.device
-
-Wants=nss-lookup.target
-Before=nss-lookup.target
-After=network.target
-
-# startup order has to be like this:
-#    dnsmasq_10.2
-#    wg-quick@wg0.service
-#    dnsmasq_10.5
-{% if net == '10.2' %}
-Before=wg-quick@wg0.service
-After=house_net.service
-{% endif %}
-{% if net == '10.5' %}
-Requires=wg-quick@wg0.service
-{% endif %}
-
-[Service]
-Type=simple
-
-# 10.5 will not work until wg0 interface is actually up, so just let it retry
-# but i think this next line was not the right way to retry.
-#SuccessExitStatus=2
-Restart=always
-RestartSec=5
-
-# Test the config file and refuse starting if it is not valid.
-ExecStartPre=/usr/sbin/dnsmasq --conf-file=/opt/dnsmasq/{{ net }}/dnsmasq.conf --test
-
-ExecStart=/usr/sbin/dnsmasq --conf-file=/opt/dnsmasq/{{ net }}/dnsmasq.conf 
-
-{% if net == '10.2' %}
-# The systemd-*-resolvconf functions configure (and deconfigure)
-# resolvconf to work with the dnsmasq DNS server. They're called like
-# this to get correct error handling (ie don't start-resolvconf if the 
-# dnsmasq daemon fails to start.
-ExecStartPost=/etc/init.d/dnsmasq systemd-start-resolvconf
-ExecStop=/etc/init.d/dnsmasq systemd-stop-resolvconf
-{% endif %}
-
-ExecReload=/bin/kill -HUP $MAINPID
-
-[Install]
-WantedBy=multi-user.target
--- a/templates/dnsmasq/hosts.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-# written by pyinfra
-
-162.243.138.136 prime-ext.bigasterisk.com public.bigasterisk.com
-
-# This is the dns trick-- hosts at home should use the local address
-# for 'bigasterisk.com' etc instead of taking a trip to prime.
-10.2.0.1 bang bang.bigasterisk.com 
-10.2.0.133 bigasterisk.com cam-int.bigasterisk.com cam-ext.bigasterisk.com imap.bigasterisk.com repo.bigasterisk.com drewp.quickwitretort.com photo.bigasterisk.com projects.bigasterisk.com quickwitretort.com whatsplayingnext.com whopickedthis.com vpn-home.bigasterisk.com file.bigasterisk.com antigen-superset.bigasterisk.com authenticate.bigasterisk.com authenticate2.bigasterisk.com authenticate3.bigasterisk.com megasecond.club hass.bigasterisk.com bitwarden.bigasterisk.com livegrep.bigasterisk.com dev.bigasterisk.com apprise.bigasterisk.com sco-bot-prefect.bigasterisk.com paperless.bigasterisk.com linkwarden.bigasterisk.com jellyfin.bigasterisk.com viseron.bigasterisk.com chat.bigasterisk.com
-
-# deleteme
-162.243.138.136 light9.bigasterisk.com
-
-# VIPs on ditto
-10.2.0.11 mqtt1 mqtt1.bigasterisk.com
-10.2.0.12 mqtt2 mqtt2.bigasterisk.com
-# might be used for syncthing
-10.2.0.13 mqtt3 mqtt3.bigasterisk.com 
-10.2.0.14 mqtt4 mqtt4.bigasterisk.com
-
-10.2.0.15 victorialogs.bigasterisk.com
-
-# sync with /my/proj/infra/inventory.py
-# and with templates/wireguard/wg0.conf.j2
-# Hosts with fixed wg0 addresses:
-10.5.0.1   bang5.bigasterisk.com local.bigasterisk.com reg 
-10.5.0.2   prime5.bigasterisk.com prime.bigasterisk.com 
-10.5.0.5   dash5.bigasterisk.com
-10.5.0.6   slash5.bigasterisk.com
-10.5.0.7   ditto5.bigasterisk.com
-10.5.0.14  ga-iot5.bigasterisk.com
-10.5.0.17  frontbed5.bigasterisk.com
-10.5.0.30  dot5.bigasterisk.com
-10.5.0.31  ws-printer5.bigasterisk.com
-10.5.0.32  gn-music5.bigasterisk.com
-10.5.0.33  li-drums5.bigasterisk.com
-10.5.0.110 plus5.bigasterisk.com
-10.5.0.111 pillow.bigasterisk.com pillow5.bigasterisk.com
-10.5.0.112 drew-note5.bigasterisk.com
-10.5.0.113 tofu.bigasterisk.com tofu5.bigasterisk.com
-
-{% if net == '10.2' %}
-# Hosts with fixed addrs who don't introduce via dhcp:
-# 162.243.138.136   prime.bigasterisk.com
-10.2.0.3 pipe pipe.bigasterisk.com
-# from netdevices.n3
-10.2.0.133 ditto ditto.bigasterisk.com
-{% endif %}
-
-{% if net == '10.5' %}
-# Names that should be routed on wg0 when the DNS lookup is on wg0:
-10.5.0.1   bang.bigasterisk.com
-10.5.0.5   dash.bigasterisk.com
-10.5.0.6   slash.bigasterisk.com
-10.5.0.7   ditto.bigasterisk.com
-10.5.0.14  ga-iot.bigasterisk.com
-10.5.0.17  frontbed.bigasterisk.com
-10.5.0.30  dot.bigasterisk.com
-10.5.0.110 plus.bigasterisk.com
-10.5.0.112 drew-note.bigasterisk.com
-{% endif %}
--- a/templates/file-count/file-count.service.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-# written by pyinfra
-
-[Unit]
-After=wg-quick@wg0.service
-
-[Service]
-Type=exec
-KillMode=process
-# port 2500 is used in victoriametrics/config/scrape_main.yaml
-ExecStart=runuser -u drewp /usr/bin/python3 /opt/file_count.py 10.5.0.2 2500 /home/drewp/Maildir/new maildir_count
-Restart=always
-RestartSec=5s
-
-[Install]
-WantedBy=multi-user.target
--- a/templates/file-count/file_count.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-import http.server
-import socketserver
-import os
-import sys
-
-interface, port, dir, metric_name = sys.argv[1:]
-
-
-class Web(http.server.SimpleHTTPRequestHandler):
-
-    def do_GET(self):
-        files_count = len(os.listdir(dir))
-        self.send_response(200)
-        self.send_header('Content-type', 'text/plain')
-        self.end_headers()
-        self.wfile.write(f'{metric_name} {files_count}'.encode())
-
-
-with socketserver.TCPServer((interface, int(port)), Web) as httpd:
-    httpd.serve_forever()
--- a/templates/hosts.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-# written by pyinfra
-
-127.0.0.1       localhost
-127.0.1.1       {{ host.name }}
-
-# The following lines are desirable for IPv6 capable hosts
-::1     ip6-localhost ip6-loopback
-fe00::0 ip6-localnet
-ff00::0 ip6-mcastprefix
-ff02::1 ip6-allnodes
-ff02::2 ip6-allrouters
-
-
-{% if 'laptop' in host.groups or 'hosted' in host.groups %}
-10.5.0.1 bang bang.bigasterisk.com bang5 bang5.bigasterisk.com 
-10.5.0.7 ditto ditto.bigasterisk.com ditto5 ditto5.bigasterisk.com 
-10.5.0.5 dash
-{% endif %}
-
-{% if host.name == 'prime' %}
-# for wireguard setup:
-127.0.0.1 public.bigasterisk.com
-{% endif %}
--- a/templates/kube/config-agent.yaml.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-node-ip: {{ wg_ip }}
-token: {{ token }}
-server: https://{{ server_ip }}:6443 
--- a/templates/kube/config-server.yaml.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-write-kubeconfig-mode: '640'
-node-ip: {{ wg_ip }}
-disable:
-  - traefik
--- a/templates/kube/coredns.yaml	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,228 +0,0 @@
-unused? needs server ip fixes
-
-
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: coredns
-  namespace: kube-system
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    kubernetes.io/bootstrapping: rbac-defaults
-  name: system:coredns
-rules:
-- apiGroups:
-  - ""
-  resources:
-  - endpoints
-  - services
-  - pods
-  - namespaces
-  verbs:
-  - list
-  - watch
-- apiGroups:
-  - discovery.k8s.io
-  resources:
-  - endpointslices
-  verbs:
-  - list
-  - watch
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
-  annotations:
-    rbac.authorization.kubernetes.io/autoupdate: "true"
-  labels:
-    kubernetes.io/bootstrapping: rbac-defaults
-  name: system:coredns
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: system:coredns
-subjects:
-- kind: ServiceAccount
-  name: coredns
-  namespace: kube-system
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: coredns
-  namespace: kube-system
-data:
-  Corefile: |
-    # update 2022-11-26T23:47
-    .:53 {
-        errors
-        health
-        ready
-        kubernetes cluster.local in-addr.arpa ip6.arpa {
-          pods insecure
-          fallthrough in-addr.arpa ip6.arpa
-        }
-        hosts /etc/coredns/NodeHosts {
-          ttl 60
-          reload 15s
-          fallthrough
-        }
-        prometheus :9153
-        forward . dns://10.2.0.3
-        cache 30
-        loop
-        reload
-        loadbalance
-        log
-    }
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: coredns
-  namespace: kube-system
-  labels:
-    k8s-app: kube-dns
-    kubernetes.io/name: "CoreDNS"
-spec:
-  #replicas: 1
-  strategy:
-    type: RollingUpdate
-    rollingUpdate:
-      maxUnavailable: 1
-  selector:
-    matchLabels:
-      k8s-app: kube-dns
-  template:
-    metadata:
-      labels:
-        k8s-app: kube-dns
-    spec:
-      priorityClassName: "system-cluster-critical"
-      serviceAccountName: coredns
-      tolerations:
-        - key: "CriticalAddonsOnly"
-          operator: "Exists"
-        - key: "node-role.kubernetes.io/control-plane"
-          operator: "Exists"
-          effect: "NoSchedule"
-        - key: "node-role.kubernetes.io/master"
-          operator: "Exists"
-          effect: "NoSchedule"
-      nodeSelector:
-        kubernetes.io/os: linux
-      affinity: # because dns is broken so often, and it might be a circular config that can't start unless this is on bang
-        nodeAffinity:
-          requiredDuringSchedulingIgnoredDuringExecution:
-            nodeSelectorTerms:
-            - matchExpressions:
-              - key: "kubernetes.io/hostname"
-                operator: In
-                values: ["bang"]
-      topologySpreadConstraints:
-        - maxSkew: 1
-          topologyKey: kubernetes.io/hostname
-          whenUnsatisfiable: DoNotSchedule
-          labelSelector:
-            matchLabels:
-              k8s-app: kube-dns
-      containers:
-      - name: coredns
-        image: rancher/mirrored-coredns-coredns:1.9.1
-        imagePullPolicy: IfNotPresent
-        resources:
-          limits:
-            memory: 170Mi
-          requests:
-            cpu: 100m
-            memory: 70Mi
-        args: [ "-conf", "/etc/coredns/Corefile" ]
-        volumeMounts:
-        - name: config-volume
-          mountPath: /etc/coredns
-          readOnly: true
-        - name: custom-config-volume
-          mountPath: /etc/coredns/custom
-          readOnly: true
-        ports:
-        - containerPort: 53
-          name: dns
-          protocol: UDP
-        - containerPort: 53
-          name: dns-tcp
-          protocol: TCP
-        - containerPort: 9153
-          name: metrics
-          protocol: TCP
-        securityContext:
-          allowPrivilegeEscalation: false
-          capabilities:
-            add:
-            - NET_BIND_SERVICE
-            drop:
-            - all
-          readOnlyRootFilesystem: true
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 8080
-            scheme: HTTP
-          initialDelaySeconds: 60
-          periodSeconds: 10
-          timeoutSeconds: 1
-          successThreshold: 1
-          failureThreshold: 3
-        readinessProbe:
-          httpGet:
-            path: /ready
-            port: 8181
-            scheme: HTTP
-          initialDelaySeconds: 0
-          periodSeconds: 2
-          timeoutSeconds: 1
-          successThreshold: 1
-          failureThreshold: 3
-      dnsPolicy: Default
-      volumes:
-        - name: config-volume
-          configMap:
-            name: coredns
-            items:
-            - key: Corefile
-              path: Corefile
-            - key: NodeHosts
-              path: NodeHosts
-        - name: custom-config-volume
-          configMap:
-            name: coredns-custom
-            optional: true
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: kube-dns
-  namespace: kube-system
-  annotations:
-    prometheus.io/port: "9153"
-    prometheus.io/scrape: "true"
-  labels:
-    k8s-app: kube-dns
-    kubernetes.io/cluster-service: "true"
-    kubernetes.io/name: "CoreDNS"
-spec:
-  selector:
-    k8s-app: kube-dns
-  clusterIP: '10.5.0.1'
-  ports:
-  - name: dns
-    port: 53
-    protocol: UDP
-  - name: dns-tcp
-    port: 53
-    protocol: TCP
-  - name: metrics
-    port: 9153
-    protocol: TCP
--- a/templates/kube/k3s.service.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-# written by pyinfra
-
-[Unit]
-Description=Lightweight Kubernetes
-Documentation=https://k3s.io
-After=network-online.target
-
-[Service]
-Type=notify
-ExecStartPre=-/sbin/modprobe br_netfilter
-ExecStartPre=-/sbin/modprobe overlay
-ExecStart=/usr/local/bin/k3s {{ role }} --config /etc/k3s_config.yaml --kubelet-arg=config=/etc/rancher/k3s/kubelet.config
-KillMode=process
-Delegate=yes
-# Having non-zero Limit*s causes performance problems due to accounting overhead
-# in the kernel. We recommend using cgroups to do container-local accounting.
-LimitNOFILE=1048576
-LimitNPROC=infinity
-LimitCORE=infinity
-TasksMax=infinity
-TimeoutStartSec=0
-Restart=always
-RestartSec=5s
-
-[Install]
-WantedBy=multi-user.target
--- a/templates/kube/podman_registries.conf.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-[[registry]]
-location = "{{reg}}"
-insecure = true
--- a/templates/kube/registries.yaml.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-# written by pyinfra
-
-
-# docs: https://rancher.com/docs/k3s/latest/en/installation/private-registry/
-
-
-mirrors:
-  "{{reg}}":
-    endpoint:
-      - "http://{{reg}}"
--- a/templates/mail/main.cf.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-# written by pyinfra
-
-compatibility_level = 3
-
-smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
-
-readme_directory = /usr/share/doc/postfix
-html_directory = /usr/share/doc/postfix/html
-
-inet_interfaces = all
-
-# TLS parameters
-smtpd_tls_cert_file=/etc/ssl/certs/self1-ca.crt
-smtpd_tls_key_file=/etc/ssl/certs/self1-ca.key
-smtpd_use_tls=yes
-smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
-smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
-smtpd_tls_loglevel = 0
-smtpd_tls_security_level = may
-smtpd_tls_received_header = yes
-smtpd_relay_before_recipient_restrictions = yes
-smtp_address_preference = ipv4
-
-# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
-# information on enabling SSL in the smtp client.
-
-relayhost = {{ 'prime.bigasterisk.com' if host.name != 'prime' else '' }}
-
-alias_maps = hash:/etc/postfix/aliases
-alias_database = hash:/etc/postfix/aliases
-
-{% if host.name == 'prime' %}
-myhostname = bigasterisk.com
-mydestination = /etc/postfix/mydestination
-{% else %}
-myhostname = {{ host.name }}.bigasterisk.com
-# must relay, even if you think you're the destination name is correct
-mydestination = 
-{% endif %}
-
-relay_domains = $mydestination
-mynetworks_style = subnet
-mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 10.1.0.0/16 10.3.0.0/16 10.5.0.0/24 192.168.0.3/32 [fc7b:54e8:69a9:e165:86c8:9d42:6cc5:b2a1]/128 [fcc8:29d:5660:ec63:754f:37af:de4a:a9df]/128
-
-# allow realuser+fakepart@bigasterisk.com
-recipient_delimiter = +
-
-{% if host.name == 'prime' %}
-# mail can only deliver on prime
-mailbox_size_limit = 0
-home_mailbox = Maildir/
-biff = no
-message_size_limit = 50000000
-#mailbox_command = procmail -a "$EXTENSION"
-{% endif %}
-
-
-# http://www.spamcop.net/fom-serve/cache/349.html
-# upgraded, per http://www.wrightthisway.com/Articles/000062.html
-
-smtpd_recipient_restrictions =
-    permit_mynetworks, 
-    permit_sasl_authenticated,
-#    check_client_access  /etc/passwd somehow?
-    reject_invalid_hostname, 
-    reject_non_fqdn_sender, 
-    reject_non_fqdn_recipient, 
-    reject_unknown_sender_domain, 
-    reject_unknown_recipient_domain, 
-    reject_unauth_pipelining, 
-    permit_tls_clientcerts,
-    reject_unauth_destination, 
-    check_sender_access hash:/etc/postfix/sender_access,
-    reject_rbl_client bl.spamcop.net,
-    permit
-    
-smtpd_tls_ask_ccert = yes
-
-# no dovecot
-smtpd_sasl_type = cyrus
-cyrus_sasl_config_path = /etc/postfix/sasl/
-
-# yes dovecot
-#smtpd_sasl_type = dovecot
-#smtpd_sasl_path = private/auth
-
-smtpd_sasl_auth_enable = yes
-smtpd_sasl_security_options = noanonymous
-smtpd_sasl_tls_security_options = $smtpd_sasl_security_options
-smtpd_tls_auth_only = yes
-
-queue_directory = /var/spool/postfix
-
-# Postfix is the final destination for the specified list
-{% if host.name == 'prime' %}
-virtual_alias_domains = adkinslawgroup.com iveseenyoubefore.com fantasyfamegame.com maxradi.us whopickedthis.com quickwitretort.com drewp.quickwitretort.com kelsi.quickwitretort.com photo.bigasterisk.com whatsplayingnext.com williamperttula.com 
-
-# Optional lookup tables that alias specific mail addresses or domains to other local or remote addresses
-virtual_alias_maps = hash:/etc/postfix/virtual
-{% endif %}
-
-smtpd_milters = inet:127.0.0.1:8891
-non_smtpd_milters = $smtpd_milters
-milter_default_action = accept
--- a/templates/mail/mydestination.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-localhost
-localhost.bigasterisk.com
-10.2.0.1
-a.mx.bigasterisk.com
-bang.bigasterisk.com
-bigast.com
-bigasterisk.com
-dash.bigasterisk.com
-mail.bigasterisk.com
-www.bigasterisk.com
-chitty.bigasterisk.com
-cuisine.bigasterisk.com
-dot.bigasterisk.com
-drewp.quickwitretort.com
-kelsi.quickwitretort.com
-maxradi.us
-williamperttula.com
-ditto.bigasterisk.com
-chat.bigasterisk.com
-
--- a/templates/mail/opendkim-KeyTable.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-default._domainkey.bigasterisk.com bigasterisk.com:default:/etc/opendkim/keys/bigasterisk.com/default.private
-default._domainkey.chat.bigasterisk.com chat.bigasterisk.com:default:/etc/opendkim/keys/chat.bigasterisk.com/default.private
--- a/templates/mail/opendkim-SigningTable.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-*@bigasterisk.com default._domainkey.bigasterisk.com
-*@chat.bigasterisk.com default._domainkey.chat.bigasterisk.com
--- a/templates/mail/opendkim-TrustedHosts.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-127.0.0.1
-::1
-*.bigasterisk.com
-10.5.0.0/16
--- a/templates/mail/opendkim.conf.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,772 +0,0 @@
-##
-## opendkim.conf -- configuration file for OpenDKIM filter
-##
-## Copyright (c) 2010-2015, 2018, The Trusted Domain Project.
-##   All rights reserved.
-##
-
-##
-## For settings that refer to a "dataset", see the opendkim(8) man page.
-##
-
-## DEPRECATED CONFIGURATION OPTIONS
-## 
-## The following configuration options are no longer valid.  They should be
-## removed from your existing configuration file to prevent potential issues.
-## Failure to do so may result in opendkim being unable to start.
-## 
-## Removed in 2.10.0:
-##   AddAllSignatureResults
-##   ADSPAction
-##   ADSPNoSuchDomain
-##   BogusPolicy
-##   DisableADSP
-##   LDAPSoftStart
-##   LocalADSP
-##   NoDiscardableMailTo
-##   On-PolicyError
-##   SendADSPReports
-##   UnprotectedPolicy
-
-## CONFIGURATION OPTIONS
-
-##  AllowSHA1Only { yes | no }
-##  	default "no"
-##
-##  By default, the filter will refuse to start if support for SHA256 is
-##  not available since this violates the strong recommendations of
-##  RFC6376 Section 3.3, which says:
-##
-##  "Verifiers MUST implement both rsa-sha1 and rsa-sha256.  Signers MUST
-##   implement and SHOULD sign using rsa-sha256."
-##
-##  This forces that violation to be explicitly selected by the administrator.
-
-# AllowSHA1Only		no
-
-##  AlwaysAddARHeader { yes | no }
-##  	default "no"
-##
-##  Add an "Authentication-Results:" header even to unsigned messages
-##  from domains with no "signs all" policy.  The reported DKIM result
-##  will be "none" in such cases.  Normally unsigned mail from non-strict
-##  domains does not cause the results header to be added.
-
-# AlwaysAddARHeader	no
-
-##  AuthservID string
-##  	default (local host name)
-##
-##  Defines the "authserv-id" token to be used when generating 
-##  Authentication-Results headers after message verification.
-
-# AuthservID		example.com
-
-##  AuthservIDWithJobID
-##  	default "no"
-##
-##  Appends a "/" followed by the MTA's job ID to the "authserv-id" token
-##  when generating Authentication-Results headers after message verification.
-
-# AuthservIDWithJobId	no
-
-##  AutoRestart { yes | no }
-##  	default "no"
-##
-##  Indicate whether or not the filter should arrange to restart automatically
-##  if it crashes.
-
-# AutoRestart		No
-
-##  AutoRestartCount n
-##  	default 0
-##
-##  Sets the maximum automatic restart count.  After this number of
-##  automatic restarts, the filter will give up and terminate.  A value of 0
-##  implies no limit.
-
-# AutoRestartCount	0
-
-##  AutoRestartRate n/t[u]
-##  	default (none)
-## 
-##  Sets the maximum automatic restart rate.  See the opendkim.conf(5)
-##  man page for the format of this parameter.
-
-# AutoRestartRate	n/tu
-
-##  Background { yes | no }
-##  	default "yes"
-##
-##  Indicate whether or not the filter should run in the background.
-
-# Background		Yes
-
-##  BaseDirectory path
-##  	default (none)
-##
-##  Causes the filter to change to the named directory before beginning
-##  operation.  Thus, cores will be dumped here and configuration files
-##  are read relative to this location.
-
-# BaseDirectory		/var/run/opendkim
-
-##  BodyLengthDB dataset
-##  	default (none)
-##
-##  A data set that is checked against envelope recipients to see if a
-##  body length tag should be included in the generated signature.
-##  This has security implications; see opendkim.conf(5) for details.
-
-# BodyLengthDB		dataset
-
-##  Canonicalization hdrcanon[/bodycanon]
-##  	default "simple/simple"
-##
-##  Select canonicalizations to use when signing.  If the "bodycanon" is
-##  omitted, "simple" is used.  Valid values for each are "simple" and
-##  "relaxed".
-
-# Canonicalization	simple/simple
-
-##  ClockDrift n
-##  	default 300
-##
-##  Specify the tolerance range for expired signatures or signatures
-##  which appear to have timestamps in the future, allowing for clock
-##  drift.
-
-# ClockDrift		300 
-
-##  Diagnostics { yes | no }
-##  	default "no"
-##
-##  Specifies whether or not signatures with header diagnostic tags should
-##  be generated.
-
-# Diagnostics		No
-
-##  DNSTimeout n
-##  	default 10
-##
-##  Specify the time in seconds to wait for replies from the nameserver when
-##  requesting keys or signing policies.
-
-# DNSTimeout		10
-
-##  Domain dataset
-##  	default (none)
-##
-##  Specify for which domain(s) signing should be done.  No default; must
-##  be specified for signing.
-
-Domain			bigasterisk.com,chat.bigasterisk.com
-
-##  DomainKeysCompat { yes | no }
-##  	default "no"
-##
-##  When enabled, backward compatibility with DomainKeys (RFC4870) key
-##  records is enabled.  Otherwise, such key records are considered to be
-##  syntactically invalid.
-
-# DomainKeysCompat	no
-
-##  DontSignMailTo	dataset
-##  	default (none)
-##
-##  Gives a list of recipient addresses or address patterns whose mail should
-##  not be signed.
-
-# DontSignMailTo	addr1,addr2,...
-
-##  EnableCoredumps { yes | no }
-##  	default "no"
-##
-##  On systems which have support for such, requests that the kernel dump
-##  core even though the process may change user ID during its execution.
-
-# EnableCoredumps	no
-
-##  ExemptDomains dataset
-##  	default (none)
-##
-##  A data set of domain names that are checked against the message sender's
-##  domain.  If a match is found, the message is ignored by the filter.
-
-# ExemptDomains		domain1,domain2,...
-
-##  ExternalIgnoreList filename
-##
-##  Names a file from which a list of externally-trusted hosts is read.
-##  These are hosts which are allowed to send mail through you for signing.
-##  Automatically contains 127.0.0.1.  See man page for file format.
-
-ExternalIgnoreList	refile:/etc/opendkim/TrustedHosts
-
-##  FixCRLF { yes | no }
-##
-##  Requests that the library convert "naked" CR and LF characters to
-##  CRLFs during canonicalization.  The default is "no".
-
-# FixCRLF 		no
-
-##  IgnoreMalformedMail { yes | no }
-##  	default "no"
-##
-##  Silently passes malformed messages without alteration.  This includes 
-##  messages that fail the RequiredHeaders check, if enabled.  The default is
-##  to pass those messages but add an Authentication-Results field indicating
-##  that they were malformed.
-
-# IgnoreMalformedMail	no
-
-##  InternalHosts dataset
-##  	default "127.0.0.1"
-##
-##  Names a file from which a list of internal hosts is read.  These are
-##  hosts from which mail should be signed rather than verified.
-##  Automatically contains 127.0.0.1.
-
-InternalHosts		refile:/etc/opendkim/TrustedHosts
-
-##  KeepTemporaryFiles { yes | no }
-##  	default "no"
-##
-##  If set, causes temporary files generated during message signing or
-##  verifying to be left behind for debugging use.  Not for normal operation;
-##  can fill your disks quite fast on busy systems.
-
-# KeepTemporaryFiles	no
-
-##  KeyFile filename
-##  	default (none)
-##
-##  Specifies the path to the private key to use when signing.  Ignored if
-##  SigningTable and KeyTable are used.  No default; must be specified for 
-##  signing if SigningTable/KeyTable are not in use.
-
-KeyFile			/etc/opendkim/keys/default.private
-
-##  KeyTable dataset
-##  	default (none)
-##
-##  Defines a table that will be queried to convert key names to
-##  sets of data of the form (signing domain, signing selector, private key).
-##  The private key can either contain a PEM-formatted private key,
-##  a base64-encoded DER format private key, or a path to a file containing
-##  one of those.
-
-KeyTable		/etc/opendkim/KeyTable
-
-##  LogWhy { yes | no }
-##  	default "no"
-##
-##  If logging is enabled (see Syslog below), issues very detailed logging
-##  about the logic behind the filter's decision to either sign a message
-##  or verify it.  The logic behind the decision is non-trivial and can be
-##  confusing to administrators not familiar with its operation.  A
-##  description of how the decision is made can be found in the OPERATIONS
-##  section of the opendkim(8) man page.  This causes a large increase
-##  in the amount of log data generated for each message, so it should be
-##  limited to debugging use and not enabled for general operation.
-
-LogWhy		yes
-
-##  MacroList macro[=value][,...]
-##
-##  Gives a set of MTA-provided macros which should be checked to see
-##  if the sender has been determined to be a local user and therefore
-##  whether or not signing should be done.  See opendkim.conf(5) for
-##  more information.
-
-# MacroList		foo=bar,baz=blivit
-
-##  MaximumHeaders n
-##
-##  Disallow messages whose header blocks are bigger than "n" bytes.
-##  Intended to detect and block a denial-of-service attack.  The default
-##  is 65536.  A value of 0 disables this test.
-
-# MaximumHeaders	n
-
-##  MaximumSignaturesToVerify n
-##  	(default 3)
-##
-##  Verify no more than "n" signatures on an arriving message.
-##  A value of 0 means "no limit".
-
-# MaximumSignaturesToVerify	n
-
-##  MaximumSignedBytes n
-##
-##  Don't sign more than "n" bytes of the message.  The default is to 
-##  sign the entire message.  Setting this implies "BodyLengths".
-
-# MaximumSignedBytes	n
-
-##  MilterDebug n
-##
-##  Request a debug level of "n" from the milter library.  The default is 0.
-
-# MilterDebug		0
-
-##  Minimum n[% | +]
-##  	default 0
-##
-##  Sets a minimum signing volume; one of the following formats:
-##	n	at least n bytes (or the whole message, whichever is less)
-##		must be signed
-##  	n%	at least n% of the message must be signed
-##	n+	if a length limit was presented in the signature, no more than
-##  		n bytes may have been added
-
-# Minimum		n
-
-##  MinimumKeyBits n
-##  	default 1024
-##
-##  Causes the library not to accept signatures matching keys made of fewer
-##  than the specified number of bits, even if they would otherwise pass
-##  DKIM signing.
-
-# MinimumKeyBits	1024
-
-##  Mode [sv]
-##  	default sv
-##
-##  Indicates which mode(s) of operation should be provided.  "s" means
-##  "sign", "v" means "verify".
-
-Mode			sv
-
-##  MTA dataset
-##  	default (none)
-##  
-##  Specifies a list of MTAs whos mail should always be signed rather than
-##  verified.  The "mtaname" is extracted from the DaemonPortOptions line
-##  in effect.
-
-# MTA			name
-
-##  MultipleSignatures { yes | no }
-##  	default no
-##
-##  Allows multiple signatures to be added.  If set to "true" and a SigningTable
-##  is in use, all SigningTable entries that match the candidate message will
-##  cause a signature to be added.  Otherwise, only the first matching
-##  SigningTable entry will be added, or only the key defined by Domain,
-##  Selector and KeyFile will be added.
-
-# MultipleSignatures	no
-
-##  MustBeSigned dataset
-##  	default (none)
-##
-##  Defines a list of headers which, if present on a message, must be
-##  signed for the signature to be considered acceptable.
-
-# MustBeSigned		header1,header2,...
-
-##  Nameservers addr1[,addr2[,...]]
-##  	default (none)
-##
-##  Provides a comma-separated list of IP addresses that are to be used when
-##  doing DNS queries to retrieve DKIM keys, VBR records, etc.
-##  These override any local defaults built in to the resolver in use, which
-##  may be defined in /etc/resolv.conf or hard-coded into the software.
-
-# Nameservers addr1,addr2,...
-
-##  NoHeaderB { yes | no }
-##  	default "no"
-##
-##  Suppresses addition of "header.b" tags on Authentication-Results
-##  header fields.
-
-# NoHeaderB		no
-
-##  OmitHeaders dataset
-##  	default (none)
-##
-##  Specifies a list of headers that should always be omitted when signing.
-##  Header names should be separated by commas.
-
-# OmitHeaders		header1,header2,...
-
-##  On-...
-##
-##  Specifies what to do when certain error conditions are encountered.
-##
-##  See opendkim.conf(5) for more information.
-
-# On-Default
-# On-BadSignature
-# On-DNSError
-# On-InternalError
-# On-NoSignature
-# On-Security
-# On-SignatureError
-
-##  OversignHeaders dataset
-##  	default (none)
-##
-##  Specifies a set of header fields that should be included in all signature
-##  header lists (the "h=" tag) once more than the number of times they were
-##  actually present in the signed message.  See opendkim.conf(5) for more
-##  information.
-
-# OverSignHeaders	header1,header2,...
-
-##  PeerList dataset
-##  	default (none)
-##
-##  Contains a list of IP addresses, CIDR blocks, hostnames or domain names
-##  whose mail should be neither signed nor verified by this filter.  See man
-##  page for file format.
-
-# PeerList		filename
-
-##  PidFile filename
-##  	default (none)
-## 
-##  Name of the file where the filter should write its pid before beginning
-##  normal operations.
-
-# PidFile		filename
-
-##  POPDBFile dataset
-##  	default (none)
-##
-##  Names a database which should be checked for "POP before SMTP" records
-##  as a form of authentication of users who may be sending mail through
-##  the MTA for signing.  Requires special compilation of the filter.
-##  See opendkim.conf(5) for more information.
-
-# POPDBFile		filename
-
-##  Quarantine { yes | no }
-##  	default "no"
-##
-##  Indicates whether or not the filter should arrange to quarantine mail
-##  which fails verification.  Intended for diagnostic use only.
-
-# Quarantine		No
-
-##  QueryCache { yes | no }
-##  	default "no"
-##
-##  Instructs the DKIM library to maintain its own local cache of keys and
-##  policies retrieved from DNS, rather than relying on the nameserver for
-##  caching service.  Useful if the nameserver being used by the filter is
-##  not local.  The filter must be compiled with the QUERY_CACHE flag to enable
-##  this feature, since it adds a library dependency.
-
-# QueryCache		No
-
-##  RedirectFailuresTo address
-##  	default (none)
-##
-##  Redirects signed messages to the specified address if none of the
-##  signatures present failed to verify.
-
-# RedirectFailuresTo	postmaster@example.com
-
-##  RemoveARAll { yes | no }
-##  	default "no"
-##
-##  Remove all Authentication-Results: headers on all arriving mail.
-
-# RemoveARAll		No
-
-##  RemoveARFrom dataset
-##  	default (none)
-##
-##  Remove all Authentication-Results: headers on all arriving mail that
-##  claim to have been added by hosts listed in this parameter.  The list
-##  should be comma-separated.  Entire domains may be specified by preceding
-##  the dopmain name by a single dot (".") character.
-
-# RemoveARFrom		host1,host2,.domain1,.domain2,...
-
-##  RemoveOldSignatures { yes | no }
-##  	default "no"
-##
-##  Remove old signatures on messages, if any, when generating a signature.
-
-# RemoveOldSignatures	No
-
-##  ReportAddress addr
-##  	default (executing user)@(hostname)
-##
-##  Specifies the sending address to be used on From: headers of outgoing
-##  failure reports.  By default, the e-mail address of the user executing
-##  the filter is used.
-
-# ReportAddress		"DKIM Error Postmaster" <postmaster@example.com>
-
-##  ReportBccAddress addr
-##  	default (none)
-##
-##  Specifies additional recipient address(es) to receive outgoing failure
-##  reports.
-
-# ReportBccAddress	postmaster@example.com, john@example.com
-
-##  RequiredHeaders { yes | no }
-##  	default no
-##
-##  Rejects messages which don't conform to RFC5322 header count requirements.
-
-# RequiredHeaders	No
-
-##  RequireSafeKeys { yes | no }
-##  	default yes
-##
-##  Refuses to use key files that appear to have unsafe permissions.
-
-# RequireSafeKeys	Yes
-RequireSafeKeys false
-
-##  ResignAll { yes | no }
-##  	default no
-##
-##  Where ResignMailTo triggers a re-signing action, this flag indicates
-##  whether or not all mail should be signed (if set) versus only verified
-##  mail being signed (if not set).
-
-# ResignAll		No
-
-##  ResignMailTo dataset
-##  	default (none)
-##
-##  Checks each message recipient against the specified dataset for a
-##  matching record.  The full address is checked in each case, then the
-##  hostname, then each domain preceded by ".".  If there is a match, the
-##  value returned is presumed to be the name of a key in the KeyTable
-##  (if defined) to be used to re-sign the message in addition to
-##  verifying it.  If there is a match without a KeyTable, the default key
-##  is applied.
-
-# ResignMailTo		dataset
-
-##  ResolverConfiguration string
-##
-##  Passes arbitrary configuration data to the resolver.  For the stock UNIX
-##  resolver, this is ignored; for Unbound, it names a resolv.conf(5)-style
-##  file that should be read for configuration information.
-
-# ResolverConfiguration	string
-
-##  ResolverTracing { yes | no }
-##
-##  Requests enabling of resolver trace features, if available.  The effect
-##  of setting this flag depends on how trace features, if any, are implemented
-##  in the resolver in use.  Currently only effective when used with the
-##  OpenDKIM asynchronous resolver.
-
-# ResolverTracing	no
-
-##  Selector name
-##
-##  The name of the selector to use when signing.  No default; must be
-##  specified for signing.
-
-Selector		default
-
-##  SenderHeaders 	dataset
-##  	default (none)
-##
-##  Overrides the default list of headers that will be used to determine
-##  the sending domain when deciding whether to sign the message and with
-##  with which key(s).  See opendkim.conf(5) for details.
-
-# SenderHeaders		From
-
-##  SendReports { yes | no }
-##  	default "no"
-##
-##  Specifies whether or not the filter should generate report mail back
-##  to senders when verification fails and an address for such a purpose
-##  is provided.  See opendkim.conf(5) for details.
-
-# SendReports		No
-
-##  SignatureAlgorithm signalg
-##  	default "rsa-sha256"
-##
-##  Signature algorithm to use when generating signatures.  Must be one of
-##  "rsa-sha1", "rsa-sha256", or "ed25519-sha256".
-
-# SignatureAlgorithm	rsa-sha256
-
-##  SignatureTTL seconds
-##  	default "0"
-##
-##  Specifies the lifetime in seconds of signatures generated by the
-##  filter.  A value of 0 means no expiration time is included in the
-##  signature.
-
-# SignatureTTL		0
-
-##  SignHeaders dataset
-##  	default (none)
-##
-##  Specifies the list of headers which should be included when generating
-##  signatures.  The string should be a comma-separated list of header names.
-##  See the opendkim.conf(5) man page for more information.
-
-# SignHeaders		header1,header2,...
-
-##  SigningTable dataset
-##  	default (none)
-##
-##  Defines a dataset that will be queried for the message sender's address
-##  to determine which private key(s) (if any) should be used to sign the
-##  message.  The sender is determined from the value of the sender
-##  header fields as described with SenderHeaders above.  The key for this
-##  lookup should be an address or address pattern that matches senders;
-##  see the opendkim.conf(5) man page for more information.  The value
-##  of the lookup should return the name of a key found in the KeyTable
-##  that should be used to sign the message.  If MultipleSignatures
-##  is set, all possible lookup keys will be attempted which may result
-##  in multiple signatures being applied.
-
-SigningTable		refile:/etc/opendkim/SigningTable
-
-##  SingleAuthResult { yes | no}
-##  	default "no"
-##
-##  When DomainKeys verification is enabled, multiple Authentication-Results
-##  will be added, one for DK and one for DKIM.  With this enabled, only
-##  a DKIM result will be reported unless DKIM failed but DK passed, in which
-##  case only a DK result will be reported.
-
-# SingleAuthResult	no
-
-##  SMTPURI uri
-##
-##  Specifies a URI (e.g., "smtp://localhost") to which mail should be sent
-##  via SMTP when notifications are generated.
-
-# Socket smtp://localhost
-
-##  Socket socketspec
-##
-##  Names the socket where this filter should listen for milter connections
-##  from the MTA.  Required.  Should be in one of these forms:
-##
-##  inet:port@address		to listen on a specific interface
-##  inet:port			to listen on all interfaces
-##  local:/path/to/socket	to listen on a UNIX domain socket
-
-Socket			inet:8891@localhost
-
-##  SoftwareHeader { yes | no }
-##  	default "no"
-##
-##  Add a DKIM-Filter header field to messages passing through this filter
-##  to identify messages it has processed.
-
-# SoftwareHeader	no
-
-##  StrictHeaders { yes | no }
-##  	default "no"
-##
-##  Requests that the DKIM library refuse to process a message whose
-##  header fields do not conform to the standards, in particular Section 3.6
-##  of RFC5322.
-
-# StrictHeaders		no
-
-##  StrictTestMode { yes | no }
-##  	default "no"
-##
-##  Selects strict CRLF mode during testing (see the "-t" command line
-##  flag in the opendkim(8) man page).  Messages for which all header
-##  fields and body lines are not CRLF-terminated are considered malformed
-##  and will produce an error.
-
-# StrictTestMode	no
-
-##  SubDomains { yes | no }
-##  	default "no"
-##
-##  Sign for subdomains as well?
-
-# SubDomains		No
-
-##  Syslog { yes | no }
-##  	default "yes"
-##
-##  Log informational and error activity to syslog?
-
-Syslog			Yes
-
-##  SyslogFacility      facility
-##  	default "mail"
-##
-##  Valid values are :
-##      auth cron daemon kern lpr mail news security syslog user uucp 
-##      local0 local1 local2 local3 local4 local5 local6 local7
-##
-##  syslog facility to be used
-
-# SyslogFacility	mail
-
-##  SyslogName          ident
-##      default "opendkim" (or the name of the executable)
-##
-##  Identifier to be prepended to all generated log entries.
-
-# SyslogName		opendkim
-
-##  SyslogSuccess { yes | no }
-##  	default "no"
-##
-##  Log success activity to syslog?
-
-# SyslogSuccess		No
-
-##  TemporaryDirectory path
-##  	default /tmp
-##
-##  Specifies which directory will be used for creating temporary files
-##  during message processing.
-
-# TemporaryDirectory	/tmp
-
-##  TestPublicKeys filename
-##  	default (none)
-##
-##  Names a file from which public keys should be read.  Intended for use
-##  only during automated testing.
-
-# TestPublicKeys	/tmp/testkeys
-
-##  TrustAnchorFile filename
-##  	default (none)
-##
-## Specifies a file from which trust anchor data should be read when doing
-## DNS queries and applying the DNSSEC protocol.  See the Unbound documentation
-## at http://unbound.net for the expected format of this file.
-
-# TrustAnchorFile	/var/named/trustanchor
-
-##  UMask mask
-##  	default (none)
-##
-##  Change the process umask for file creation to the specified value.
-##  The system has its own default which will be used (usually 022).
-##  See the umask(2) man page for more information.
-
-# UMask			022
-
-# UnboundConfigFile	/var/named/unbound.conf
-
-##  Userid userid
-##  	default (none)
-##
-##  Change to user "userid" before starting normal operation?  May include
-##  a group ID as well, separated from the userid by a colon.
-
-UserID		opendkim
--- a/templates/mail/opendkim.service.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-[Unit]
-Description=OpenDKIM Milter
-Documentation=man:opendkim(8) man:opendkim.conf(5) man:opendkim-lua(3) man:opendkim-genkey(8) man:opendkim-genzone(8) man:opendkim-testkey(8) http://www.opendkim.org/docs.html
-After=network-online.target nss-lookup.target
-Wants=network-online.target
-
-[Service]
-Type=forking
-#PIDFile=/run/opendkim/opendkim.pid
-ExecStart=/usr/sbin/opendkim -vv
-#ExecReload=/bin/kill -USR1 $MAINPID
-Restart=on-failure
-
-[Install]
-WantedBy=multi-user.target
\ No newline at end of file
--- a/templates/resolved.conf.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-# written by pyinfra
-
-# See resolved.conf(5) for details
-
-{% if host.name == 'prime' %}
-[Resolve]
-# This list is tried randomly, not in order, so we could have
-# some trouble with internal names
-DNS=10.5.0.1 8.8.8.8 8.8.4.4
-Domains=bigasterisk.com
-
-{% else %}
-[Resolve]
-# worst case- you might get a better one over DHCP, which would get listed AFTER this one so it needs to be the only one.
-#DNS=10.2.0.4
-#FallbackDNS=
-Domains=bigasterisk.com
-#LLMNR=no
-#MulticastDNS=no
-#DNSSEC=no
-#DNSOverTLS=no
-#Cache=yes
-#DNSStubListener=yes
-#ReadEtcHosts=yes
-{% endif %}
\ No newline at end of file
--- a/templates/webforward.service.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-# written by pyinfra
-
-[Unit]
-Description={{ fam }} forward for port {{ port }}
-Requires=network.target
-Wants=nss-lookup.target
-Before=nss-lookup.target
-After=network.target
-
-[Service]
-Type=simple
-
-ExecStart=/usr/bin/socat {{ 'tcp' if fam=='tcp' else 'udp4' }}-listen:{{ port }},fork,reuseaddr {{ 'tcp' if fam=='tcp' else 'udp' }}:{{serv_host}}:{{ port }}
-
-[Install]
-WantedBy=multi-user.target
--- a/templates/wireguard/bogasterisk.conf.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-# written by pyinfra
-
-[Interface]
-# {{ host.name }}
-Address = 10.7.0.2/16
-PrivateKey = {{priv_key}}
-ListenPort = 2113
-
-{{ peer_block('monk',             '10.7.0.42/32') }}
-{{ peer_block('firebert (phone)', '10.7.0.88/32') }}
-{{ peer_block('bird',             '10.7.0.46/32') }}
-{{ peer_block('pixel7',           '10.7.0.77/32') }}
--- a/templates/wireguard/wg.service.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-# written by pyinfra
-
-[Unit]
-Description=WireGuard via wg-quick(8) for {{wireguard_interface}}
-After=network-online.target nss-lookup.target
-Wants=network-online.target nss-lookup.target
-PartOf=wg-quick.target
-Documentation=man:wg-quick(8)
-Documentation=man:wg(8)
-Documentation=https://www.wireguard.com/
-Documentation=https://www.wireguard.com/quickstart/
-Documentation=https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8
-Documentation=https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8
-
-[Service]
-Type=oneshot
-RemainAfterExit=yes
-ExecStart=/usr/bin/wg-quick up {{wireguard_interface}}
-ExecStop=/usr/bin/wg-quick down {{wireguard_interface}}
-ExecReload=/bin/bash -c 'exec /usr/bin/wg syncconf {{wireguard_interface}} <(exec /usr/bin/wg-quick strip {{wireguard_interface}})'
-Environment=WG_ENDPOINT_RESOLUTION_RETRIES=infinity
-
-[Install]
-WantedBy=multi-user.target
\ No newline at end of file
--- a/templates/wireguard/wg0.conf.j2	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-# written by pyinfra
-
-[Interface]
-# {{ host.name }}
-Address = {{wireguard_ip}}/24
-PrivateKey = {{priv_key}}
-ListenPort = 1195
-
-# suggested by https://i.reddit.com/r/WireGuard/comments/jcwleo/ubuntu_2004_lts_server_as_wireguard_client/
-#FwMark = 0x4000
-
-{% if host.name == 'ditto' %}
-    {{ peer_block('bang',        '10.5.0.1/32') }}
-    {{ peer_block('dash',        '10.5.0.5/32') }}
-    {{ peer_block('dot',         '10.5.0.30/32') }}
-    {{ peer_block('pipe',        '10.5.0.3/32') }}
-    {{ peer_block('prime',       '10.5.0.0/24', 'public.bigasterisk.com:1195', 50) }}
-    {{ peer_block('slash',       '10.5.0.6/32') }}
-    {{ peer_block('ws-printer',  '10.5.0.31/32') }}
-    {{ peer_block('gn-music',    '10.5.0.32/32') }}
-    {{ peer_block('li-drums',    '10.5.0.33/32') }}
-    {{ peer_block('ga-iot',      '10.5.0.14/32') }}
-{% elif host.name == 'prime' %}
-# this list is wg_roamer & ditto & phone:
-    {{ peer_block('ditto',       '10.5.0.0/24') }}
-    {{ peer_block('drew-note10', '10.5.0.112/32') }}
-    {{ peer_block('plus',        '10.5.0.110/32', 'public.bigasterisk.com:1195') }}
-    {{ peer_block('pillow',      '10.5.0.111/32', 'public.bigasterisk.com:1195') }}
-    {{ peer_block('tofu',        '10.5.0.113/32', 'public.bigasterisk.com:1195') }}
-{% elif host.data.get('wg_roamer') %}
-    {{ peer_block('prime',       '10.5.0.0/24', 'public.bigasterisk.com:1195', 50) }}
-{% else %}
-# note that hosts on filtered dns cannot currently look up the name 'ditto'
-    {{ peer_block('ditto',        '10.5.0.0/24', '10.2.0.133:1195', 50) }}
-{% endif %}
--- a/users.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-from pyinfra import host
-from pyinfra.operations import server
-from pyinfra.facts.server import LinuxDistribution
-
-
-# raspbian took 1000 for 'pi' group, but drewp is rarely used on pi
-# setups so hopefully it won't matter much that drew group has a
-# different id.
-drewp_uid, drewp_gid = host.data.drewp_uid, host.data.drewp_gid
-drewp_groups = [
-    'lp', 'adm', 'dialout', 'cdrom', 'sudo', 'audio', 'video', 'plugdev',
-    'games', 'users', 'netdev', 'i2c', 'input', 'spi', 'gpio', 'fuse',
-    'render', 'mongodb', 'lpadmin'
-]
-
-for group in [
-        'fuse',
-        'spi',
-        'gpio',
-        'i2c',
-        'input',
-        'netdev',
-        'render',
-        'lpadmin',
-]:
-    server.group(group=group, system=True)
-
-svcIds = 1050
-for svc in [
-    # only append to this list:
-    "photoprism",
-    "mongodb",
-]:
-    server.group(group=svc, gid=svcIds)
-    server.user(user=svc, uid=svcIds, group=svc)
-    svcIds += 1
-
-# the following gets scrambled on new rpi.  Run "useradd -u 1501 drewp" as workaround.
-server.group(group='drewp', gid=drewp_gid)
-# this won't change existing drewp uid; I've been doing that myself.
-server.user(user='drewp', uid=drewp_uid, group='drewp', groups=drewp_groups)
-
-if 'pi' not in host.groups:
-    server.group(group='adm', gid=4)
-    server.group(group='cdrom', gid=24)
-    server.group(group='dialout', gid=20)
-    server.group(group='dip', gid=30)
-    server.group(group='lp', gid=7)
-    # prime has something on 109
-    server.group(group='lpadmin', gid=200)
-    server.group(group='plugdev', gid=46)
-
-
-    server.user(user='drewp',
-                uid=drewp_uid,
-                group='drewp',
-                groups=drewp_groups)
-
-    for name, uid, gid in [
-            ('ari', 3019, 3019),
-            ('talia', 1003, 1003),
-            ]:
-        server.group(group=name, gid=gid)
-        server.user(user=name,
-                uid=uid,
-                group=name,
-                groups=['audio', 'dialout', 'docker', 'lp', 'lpadmin', 'sudo', 'video'])
-
-    server.user(user='dmcc', uid=1013)
-
-    server.group(group='elastic', gid=3018)
-    server.user(user='elastic', uid=3018, group='elastic')
-
-    server.group(group='kelsi', gid=1008)
-    server.user(user='kelsi', uid=1008, group='elastic')
-
-    server.group(group='drewnote', gid=1009)
-    server.user(user='drewnote', uid=1009)
-
-    server.group(group='prometheus', gid=1010)
-    server.user(user='prometheus', uid=1010)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/users/users.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,83 @@
+from pyinfra.context import host
+from pyinfra.operations import server
+
+
+def setupUsers():
+    # raspbian took 1000 for 'pi' group, but drewp is rarely used on pi
+    # setups so hopefully it won't matter much that drew group has a
+    # different id.
+    drewp_uid, drewp_gid = host.data.drewp_uid, host.data.drewp_gid
+    drewp_groups = [
+        'lp', 'adm', 'dialout', 'cdrom', 'sudo', 'audio', 'video', 'plugdev', 'games', 'users', 'netdev', 'i2c', 'input', 'spi',
+        'gpio', 'fuse', 'render', 'mongodb', 'lpadmin'
+    ]
+
+    svcIds = 1050
+    for svc in [
+            # only append to this list:
+            "photoprism",
+            "mongodb",
+    ]:
+        server.group(group=svc, gid=svcIds)
+        server.user(user=svc, uid=svcIds, group=svc)
+        svcIds += 1
+
+    # the following gets scrambled on new rpi.  Run "useradd -u 1501 drewp" as workaround.
+    server.group(group='drewp', gid=drewp_gid)
+    # this won't change existing drewp uid; I've been doing that myself.
+    server.user(user='drewp', uid=drewp_uid, group='drewp', groups=drewp_groups)
+
+    if 'pi' not in host.groups:
+
+        server.user(user='drewp', uid=drewp_uid, group='drewp', groups=drewp_groups)
+
+        for name, uid, gid in [
+            ('ari', 3019, 3019),
+            ('talia', 1003, 1003),
+        ]:
+            server.group(group=name, gid=gid)
+            server.user(user=name, uid=uid, group=name, groups=['audio', 'dialout', 'docker', 'lp', 'lpadmin', 'sudo', 'video'])
+
+        server.user(user='dmcc', uid=1013)
+
+        server.group(group='elastic', gid=3018)
+        server.user(user='elastic', uid=3018, group='elastic')
+
+        server.group(group='kelsi', gid=1008)
+        server.user(user='kelsi', uid=1008, group='elastic')
+
+        server.group(group='drewnote', gid=1009)
+        server.user(user='drewnote', uid=1009)
+
+        server.group(group='prometheus', gid=1010)
+        server.user(user='prometheus', uid=1010)
+
+
+def systemGroups():
+    for group in [
+            'fuse',
+            'spi',
+            'gpio',
+            'i2c',
+            'input',
+            'netdev',
+            'render',
+            'lpadmin',
+    ]:
+        server.group(group=group, system=True)
+
+    if 'pi' not in host.groups:
+        server.group(group='adm', gid=4)
+        server.group(group='cdrom', gid=24)
+        server.group(group='dialout', gid=20)
+        server.group(group='dip', gid=30)
+        server.group(group='lp', gid=7)
+        # prime has something on 109
+        server.group(group='lpadmin', gid=200)
+        server.group(group='plugdev', gid=46)
+
+
+operations = [
+    systemGroups,
+    setupUsers,
+]
--- a/wireguard.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +0,0 @@
-import subprocess
-
-from pyinfra import host
-from pyinfra.facts.files import FindInFile
-from pyinfra.operations import files, systemd
-
-import wireguard_pubkey
-
-# other options:
-#   https://www.reddit.com/r/WireGuard/comments/fkr240/shortest_path_between_peers/
-#   https://github.com/k4yt3x/wireguard-mesh-configurator
-#   https://github.com/mawalu/wireguard-private-networking
-#
-
-
-def peer_block(hostname, allowed_ips, endpoint=None, keepalive=None):
-    # allowed_ips should be determined mostly from host.data.wireguard_address
-
-    public_key = wireguard_pubkey.pubkey[hostname]
-    out = f'''\
-
-[Peer]
-# {hostname}
-PublicKey = {public_key}
-AllowedIPs = {allowed_ips}
-'''
-    if endpoint is not None:
-        out += f'Endpoint = {endpoint}\n'
-    if keepalive is not None:
-        out += f'PersistentKeepalive = {keepalive}\n'
-    return out
-
-
-def get_priv_key(wireguard_interface) -> str:
-    priv_key_lines = host.get_fact(FindInFile, path=f'/etc/wireguard/{wireguard_interface}.conf', pattern=r'PrivateKey.*')
-    if not priv_key_lines:
-        priv_key = subprocess.check_output(['wg', 'genkey']).strip().decode('ascii')
-    else:
-        priv_key = priv_key_lines[0].split(' = ')[1]
-    return priv_key
-
-
-def compute_pub_key(priv_key: str) -> str:
-    pub_key = subprocess.check_output(['wg', 'pubkey'], input=priv_key.encode('ascii')).strip().decode('ascii')
-    # todo: if this was new, it should be added to a file of pubkeys that
-    # peer_block can refer to. meanwhile, edit the template.
-    return pub_key
-
-
-for wireguard_interface in ['wg0', 'bogasterisk']:
-    if wireguard_interface == 'bogasterisk' and host.name != 'prime':
-        continue
-
-    # note- this is specific to the wg0 setup. Other conf files don't use it.
-    wireguard_ip = host.host_data.get('wireguard_address')
-    if wireguard_interface == 'wg0' and wireguard_ip is None:
-        continue
-
-    # new pi may fail with 'Unable to access interface: Protocol not supported'. reboot fixes.
-
-    priv_key = get_priv_key(wireguard_interface)
-
-    # unused since I still hand-maintain wireguard_pubkey.py :(
-    # pub_key = compute_pub_key(priv_key)
-
-    files.template(
-        src=f'templates/wireguard/{wireguard_interface}.conf.j2',
-        dest=f'/etc/wireguard/{wireguard_interface}.conf',
-        mode='600',
-        wireguard_ip=wireguard_ip,
-        priv_key=priv_key,
-        peer_block=peer_block,
-    )
-    svc = f'wg-quick@{wireguard_interface}.service'
-
-    files.template(src='templates/wireguard/wg.service.j2',
-                   dest=f'/etc/systemd/system/{svc}',
-                   wireguard_interface=wireguard_interface)
-    systemd.service(service=svc, daemon_reload=True, restarted=True, enabled=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wireguard/templates/bogasterisk.conf.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,12 @@
+# written by pyinfra
+
+[Interface]
+# {{ host.name }}
+Address = 10.7.0.2/16
+PrivateKey = {{priv_key}}
+ListenPort = 2113
+
+{{ peer_block('monk',             '10.7.0.42/32') }}
+{{ peer_block('firebert (phone)', '10.7.0.88/32') }}
+{{ peer_block('bird',             '10.7.0.46/32') }}
+{{ peer_block('pixel7',           '10.7.0.77/32') }}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wireguard/templates/wg.service.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,24 @@
+# written by pyinfra
+
+[Unit]
+Description=WireGuard via wg-quick(8) for {{wireguard_interface}}
+After=network-online.target nss-lookup.target
+Wants=network-online.target nss-lookup.target
+PartOf=wg-quick.target
+Documentation=man:wg-quick(8)
+Documentation=man:wg(8)
+Documentation=https://www.wireguard.com/
+Documentation=https://www.wireguard.com/quickstart/
+Documentation=https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8
+Documentation=https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/usr/bin/wg-quick up {{wireguard_interface}}
+ExecStop=/usr/bin/wg-quick down {{wireguard_interface}}
+ExecReload=/bin/bash -c 'exec /usr/bin/wg syncconf {{wireguard_interface}} <(exec /usr/bin/wg-quick strip {{wireguard_interface}})'
+Environment=WG_ENDPOINT_RESOLUTION_RETRIES=infinity
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wireguard/templates/wg0.conf.j2	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,35 @@
+# written by pyinfra
+
+[Interface]
+# {{ host.name }}
+Address = {{wireguard_ip}}/24
+PrivateKey = {{priv_key}}
+ListenPort = 1195
+
+# suggested by https://i.reddit.com/r/WireGuard/comments/jcwleo/ubuntu_2004_lts_server_as_wireguard_client/
+#FwMark = 0x4000
+
+{% if host.name == 'ditto' %}
+    {{ peer_block('bang',        '10.5.0.1/32') }}
+    {{ peer_block('dash',        '10.5.0.5/32') }}
+    {{ peer_block('dot',         '10.5.0.30/32') }}
+    {{ peer_block('pipe',        '10.5.0.3/32') }}
+    {{ peer_block('prime',       '10.5.0.0/24', 'public.bigasterisk.com:1195', 50) }}
+    {{ peer_block('slash',       '10.5.0.6/32') }}
+    {{ peer_block('ws-printer',  '10.5.0.31/32') }}
+    {{ peer_block('gn-music',    '10.5.0.32/32') }}
+    {{ peer_block('li-drums',    '10.5.0.33/32') }}
+    {{ peer_block('ga-iot',      '10.5.0.14/32') }}
+{% elif host.name == 'prime' %}
+# this list is wg_roamer & ditto & phone:
+    {{ peer_block('ditto',       '10.5.0.0/24') }}
+    {{ peer_block('drew-note10', '10.5.0.112/32') }}
+    {{ peer_block('plus',        '10.5.0.110/32', 'public.bigasterisk.com:1195') }}
+    {{ peer_block('pillow',      '10.5.0.111/32', 'public.bigasterisk.com:1195') }}
+    {{ peer_block('tofu',        '10.5.0.113/32', 'public.bigasterisk.com:1195') }}
+{% elif host.data.get('wg_roamer') %}
+    {{ peer_block('prime',       '10.5.0.0/24', 'public.bigasterisk.com:1195', 50) }}
+{% else %}
+# note that hosts on filtered dns cannot currently look up the name 'ditto'
+    {{ peer_block('ditto',        '10.5.0.0/24', '10.2.0.133:1195', 50) }}
+{% endif %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wireguard/wireguard.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,85 @@
+import subprocess
+
+from pyinfra.context import host
+from pyinfra.facts.files import FindInFile
+from pyinfra.operations import files, systemd
+
+import wireguard.wireguard_pubkey as wireguard_pubkey
+
+# other options:
+#   https://www.reddit.com/r/WireGuard/comments/fkr240/shortest_path_between_peers/
+#   https://github.com/k4yt3x/wireguard-mesh-configurator
+#   https://github.com/mawalu/wireguard-private-networking
+#
+
+
+def peer_block(hostname, allowed_ips, endpoint=None, keepalive=None):
+    # allowed_ips should be determined mostly from host.data.wireguard_address
+
+    public_key = wireguard_pubkey.pubkey[hostname]
+    out = f'''\
+
+[Peer]
+# {hostname}
+PublicKey = {public_key}
+AllowedIPs = {allowed_ips}
+'''
+    if endpoint is not None:
+        out += f'Endpoint = {endpoint}\n'
+    if keepalive is not None:
+        out += f'PersistentKeepalive = {keepalive}\n'
+    return out
+
+
+def get_priv_key(wireguard_interface) -> str:
+    priv_key_lines = host.get_fact(FindInFile, path=f'/etc/wireguard/{wireguard_interface}.conf', pattern=r'PrivateKey.*')
+    if not priv_key_lines:
+        priv_key = subprocess.check_output(['wg', 'genkey']).strip().decode('ascii')
+    else:
+        priv_key = priv_key_lines[0].split(' = ')[1]
+    return priv_key
+
+
+def compute_pub_key(priv_key: str) -> str:
+    pub_key = subprocess.check_output(['wg', 'pubkey'], input=priv_key.encode('ascii')).strip().decode('ascii')
+    # todo: if this was new, it should be added to a file of pubkeys that
+    # peer_block can refer to. meanwhile, edit the template.
+    return pub_key
+
+
+def wireguard():
+    for wireguard_interface in ['wg0', 'bogasterisk']:
+        if wireguard_interface == 'bogasterisk' and host.name != 'prime':
+            continue
+
+        # note- this is specific to the wg0 setup. Other conf files don't use it.
+        wireguard_ip = host.host_data.get('wireguard_address')
+        if wireguard_interface == 'wg0' and wireguard_ip is None:
+            continue
+
+        # new pi may fail with 'Unable to access interface: Protocol not supported'. reboot fixes.
+
+        priv_key = get_priv_key(wireguard_interface)
+
+        # unused since I still hand-maintain wireguard_pubkey.py :(
+        # pub_key = compute_pub_key(priv_key)
+
+        files.template(
+            src=f'wireguard/templates/{wireguard_interface}.conf.j2',
+            dest=f'/etc/wireguard/{wireguard_interface}.conf',
+            mode='600',
+            wireguard_ip=wireguard_ip,
+            priv_key=priv_key,
+            peer_block=peer_block,
+        )
+        svc = f'wg-quick@{wireguard_interface}.service'
+
+        files.template(src='wireguard/templates/wg.service.j2',
+                       dest=f'/etc/systemd/system/{svc}',
+                       wireguard_interface=wireguard_interface)
+        systemd.service(service=svc, daemon_reload=True, restarted=True, enabled=True)
+
+
+operations = [
+    wireguard,
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wireguard/wireguard_pubkey.py	Mon Jan 20 21:55:08 2025 -0800
@@ -0,0 +1,25 @@
+pubkey = {
+    'bang': 'xDkAqfljmeVj7bB6VslxD/vVwlUh/vLXX5Wo7ZCoTQ4=',
+    'dash': 'YqcNr+QImfHqhyYH7vUYCodaGmFJkb+XGYD8TL4Ejlk=',
+    'ditto': 'IaOJzsn+KK9SuNzn8lJfaD/dgu4Otp094SK0Xz4i4VA=',
+    'dot': '0youwd1ZHBQgbd+YUg8WdxhSqiK2rSKxmzDpf+gu4z0=',
+    'drew-note10': 'QMgx4cmuUTfJ7RH4Q46b54tSQl4eISOmdEney17fnE8=',
+    'frontbed': 'ENhRhEgGaFfwV74MqYBHJgkOFpNAF5kVHVK5/tRVTjU=',
+    'ga-iot': 'oqbSKq3CNTApp9g5sEwYZhofLy2jgWjXi00H5sTSgQs=',
+    'pipe': 'yI0zt8/+baHjadhiBCX6u8sSkhjoh/Q5cIZkGf1H6S4=',
+    'plus': 'hRCwLRUGY3hYNHwsmxSmAPWqAvMr+ZM6IVAte8tLVyU=',
+    'prime': 'vR9lfsUSOIMxkY/k2gRJ6E8ZudccfPpVhrbE9zuxalU=',
+    'slash': 'dZSvwUPLKPrBWY66o8GNeWCcol6lK5QG80HLtOnCRko=',
+    'pillow': 'gi54uHkV3WQWvU7b90oZV9ss69kqyeDerkaRk1dYziU=',
+    'ws-printer': 'HGQkw9ayf4g73s8Rvj76nUCIzr5oDqNVx3GuGq6Xvnw=',
+    'li-drums': 'CkFzBGjSJLHnR7FeWzandx2F03x5tncaqpCuiNcIoCc=',
+    'gn-music': 'XKkjSfdvROkLe0zxp9wal+ObTWqh/o7kJTXL8O9AOSQ=',
+    'tofu': 'abh0iwycB8kPY1yiI18dppil7/20/IskaHMRboTg2Qg=',
+}
+
+pubkey.update({
+    'bird': '9CkgqeAiX1GhNM+t9m2nJD5QJHx9iTCFRB5c1x7h704=',
+    'firebert (phone)': 'Rr9N6dGbMLzl6wuEJlaq67gNQ5QW2ZcwD4Brn/3XJyA=',
+    'monk': 'aroc8MNdTnKg175HYxri+Yr1afuaC0awyr6TfGMpvxI=',
+    'pixel7': 'RMY3wgh/xA98aU85qE7qnFk2wStGbXAcMOl28gqu2zo=',
+})
--- a/wireguard_pubkey.py	Mon Jan 20 14:10:19 2025 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-pubkey = {
-    'bang': 'xDkAqfljmeVj7bB6VslxD/vVwlUh/vLXX5Wo7ZCoTQ4=',
-    'dash': 'YqcNr+QImfHqhyYH7vUYCodaGmFJkb+XGYD8TL4Ejlk=',
-    'ditto': 'IaOJzsn+KK9SuNzn8lJfaD/dgu4Otp094SK0Xz4i4VA=',
-    'dot': '0youwd1ZHBQgbd+YUg8WdxhSqiK2rSKxmzDpf+gu4z0=',
-    'drew-note10': 'QMgx4cmuUTfJ7RH4Q46b54tSQl4eISOmdEney17fnE8=',
-    'frontbed': 'ENhRhEgGaFfwV74MqYBHJgkOFpNAF5kVHVK5/tRVTjU=',
-    'ga-iot': 'oqbSKq3CNTApp9g5sEwYZhofLy2jgWjXi00H5sTSgQs=',
-    'pipe': 'yI0zt8/+baHjadhiBCX6u8sSkhjoh/Q5cIZkGf1H6S4=',
-    'plus': 'hRCwLRUGY3hYNHwsmxSmAPWqAvMr+ZM6IVAte8tLVyU=',
-    'prime': 'vR9lfsUSOIMxkY/k2gRJ6E8ZudccfPpVhrbE9zuxalU=',
-    'slash': 'dZSvwUPLKPrBWY66o8GNeWCcol6lK5QG80HLtOnCRko=',
-    'pillow': 'gi54uHkV3WQWvU7b90oZV9ss69kqyeDerkaRk1dYziU=',
-    'ws-printer': 'HGQkw9ayf4g73s8Rvj76nUCIzr5oDqNVx3GuGq6Xvnw=',
-    'li-drums': 'CkFzBGjSJLHnR7FeWzandx2F03x5tncaqpCuiNcIoCc=',
-    'gn-music': 'XKkjSfdvROkLe0zxp9wal+ObTWqh/o7kJTXL8O9AOSQ=',
-    'tofu': 'abh0iwycB8kPY1yiI18dppil7/20/IskaHMRboTg2Qg=',
-}
-
-pubkey.update({
-    'bird': '9CkgqeAiX1GhNM+t9m2nJD5QJHx9iTCFRB5c1x7h704=',
-    'firebert (phone)': 'Rr9N6dGbMLzl6wuEJlaq67gNQ5QW2ZcwD4Brn/3XJyA=',
-    'monk': 'aroc8MNdTnKg175HYxri+Yr1afuaC0awyr6TfGMpvxI=',
-    'pixel7': 'RMY3wgh/xA98aU85qE7qnFk2wStGbXAcMOl28gqu2zo=',
-})