@homeflare/alchemy 0.15.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/linux/directory-lifecycle.d.ts +50 -0
- package/dist/linux/directory-lifecycle.d.ts.map +1 -0
- package/dist/linux/directory.d.ts +19 -0
- package/dist/linux/directory.d.ts.map +1 -0
- package/dist/linux/fake-linux-host.d.ts +35 -0
- package/dist/linux/fake-linux-host.d.ts.map +1 -0
- package/dist/linux/fake-systemd.d.ts +34 -0
- package/dist/linux/fake-systemd.d.ts.map +1 -0
- package/dist/linux/index.d.ts +27 -0
- package/dist/linux/index.d.ts.map +1 -0
- package/dist/linux/index.js +1554 -0
- package/dist/linux/index.js.map +37 -0
- package/dist/linux/providers.d.ts +17 -0
- package/dist/linux/providers.d.ts.map +1 -0
- package/dist/linux/region.d.ts +43 -0
- package/dist/linux/region.d.ts.map +1 -0
- package/dist/linux/remote-file-form.d.ts +47 -0
- package/dist/linux/remote-file-form.d.ts.map +1 -0
- package/dist/linux/remote-file-lifecycle.d.ts +36 -0
- package/dist/linux/remote-file-lifecycle.d.ts.map +1 -0
- package/dist/linux/remote-file-plan.d.ts +33 -0
- package/dist/linux/remote-file-plan.d.ts.map +1 -0
- package/dist/linux/remote-file.d.ts +28 -0
- package/dist/linux/remote-file.d.ts.map +1 -0
- package/dist/linux/ssh-command.d.ts +68 -0
- package/dist/linux/ssh-command.d.ts.map +1 -0
- package/dist/linux/ssh-runner.d.ts +22 -0
- package/dist/linux/ssh-runner.d.ts.map +1 -0
- package/dist/linux/ssh-scripts.d.ts +27 -0
- package/dist/linux/ssh-scripts.d.ts.map +1 -0
- package/dist/linux/systemctl.d.ts +67 -0
- package/dist/linux/systemctl.d.ts.map +1 -0
- package/dist/linux/unit-form.d.ts +78 -0
- package/dist/linux/unit-form.d.ts.map +1 -0
- package/dist/linux/unit-handlers.d.ts +15 -0
- package/dist/linux/unit-handlers.d.ts.map +1 -0
- package/dist/linux/unit-lifecycle.d.ts +44 -0
- package/dist/linux/unit-lifecycle.d.ts.map +1 -0
- package/dist/linux/unit-settle.d.ts +31 -0
- package/dist/linux/unit-settle.d.ts.map +1 -0
- package/dist/linux/unit.d.ts +35 -0
- package/dist/linux/unit.d.ts.map +1 -0
- package/dist/netbox/client.d.ts +52 -0
- package/dist/netbox/client.d.ts.map +1 -0
- package/dist/netbox/constraint-guard.d.ts +26 -0
- package/dist/netbox/constraint-guard.d.ts.map +1 -0
- package/dist/netbox/constraints.d.ts +76 -0
- package/dist/netbox/constraints.d.ts.map +1 -0
- package/dist/netbox/generated/constraints/index.d.ts +19 -0
- package/dist/netbox/generated/constraints/index.d.ts.map +1 -0
- package/dist/netbox/generated/constraints/netbox-ipam.d.ts +21 -0
- package/dist/netbox/generated/constraints/netbox-ipam.d.ts.map +1 -0
- package/dist/netbox/index.d.ts +17 -0
- package/dist/netbox/index.d.ts.map +1 -0
- package/dist/netbox/index.js +331 -0
- package/dist/netbox/index.js.map +18 -0
- package/dist/netbox/prefix-form.d.ts +54 -0
- package/dist/netbox/prefix-form.d.ts.map +1 -0
- package/dist/netbox/prefix.d.ts +80 -0
- package/dist/netbox/prefix.d.ts.map +1 -0
- package/dist/netbox/resource.d.ts +94 -0
- package/dist/netbox/resource.d.ts.map +1 -0
- package/dist/netbox/values.d.ts +24 -0
- package/dist/netbox/values.d.ts.map +1 -0
- package/dist/proxmox/api-token-form.d.ts.map +1 -1
- package/dist/proxmox/apply-endpoints.d.ts +30 -0
- package/dist/proxmox/apply-endpoints.d.ts.map +1 -0
- package/dist/proxmox/ceph-daemon-form.d.ts +29 -1
- package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -1
- package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
- package/dist/proxmox/ceph-flag.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs.d.ts.map +1 -1
- package/dist/proxmox/ceph-osd-write.d.ts +19 -0
- package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
- package/dist/proxmox/ceph-pool.d.ts.map +1 -1
- package/dist/proxmox/constraints.d.ts +28 -5
- package/dist/proxmox/constraints.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pbs-config.d.ts +6 -1
- package/dist/proxmox/generated/constraints/pbs-config.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pve-access.d.ts +6 -1
- package/dist/proxmox/generated/constraints/pve-access.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-pools.d.ts +6 -1
- package/dist/proxmox/generated/constraints/pve-pools.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pve-storage.d.ts +6 -1
- package/dist/proxmox/generated/constraints/pve-storage.d.ts.map +1 -1
- package/dist/proxmox/generated/pbs/access-users.d.ts +137 -0
- package/dist/proxmox/generated/pbs/access-users.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/access.d.ts +171 -0
- package/dist/proxmox/generated/pbs/access.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/admin-datastore-store.d.ts +252 -0
- package/dist/proxmox/generated/pbs/admin-datastore-store.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/admin-metrics.d.ts +147 -0
- package/dist/proxmox/generated/pbs/admin-metrics.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/admin.d.ts +217 -0
- package/dist/proxmox/generated/pbs/admin.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/backup.d.ts +114 -0
- package/dist/proxmox/generated/pbs/backup.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-access-pam.d.ts +218 -0
- package/dist/proxmox/generated/pbs/config-access-pam.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-access.d.ts +203 -0
- package/dist/proxmox/generated/pbs/config-access.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-datastore.d.ts +195 -0
- package/dist/proxmox/generated/pbs/config-datastore.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-media-pool.d.ts +207 -0
- package/dist/proxmox/generated/pbs/config-media-pool.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-notifications-matchers.d.ts +159 -0
- package/dist/proxmox/generated/pbs/config-notifications-matchers.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-notifications.d.ts +199 -0
- package/dist/proxmox/generated/pbs/config-notifications.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-remote.d.ts +175 -0
- package/dist/proxmox/generated/pbs/config-remote.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-sync.d.ts +145 -0
- package/dist/proxmox/generated/pbs/config-sync.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-tape.d.ts +200 -0
- package/dist/proxmox/generated/pbs/config-tape.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config-verify.d.ts +77 -0
- package/dist/proxmox/generated/pbs/config-verify.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/config.d.ts +119 -0
- package/dist/proxmox/generated/pbs/config.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/nodes-node-dns.d.ts +178 -0
- package/dist/proxmox/generated/pbs/nodes-node-dns.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/nodes-node-services.d.ts +205 -0
- package/dist/proxmox/generated/pbs/nodes-node-services.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/nodes-node.d.ts +166 -0
- package/dist/proxmox/generated/pbs/nodes-node.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/nodes.d.ts +171 -0
- package/dist/proxmox/generated/pbs/nodes.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/ping.d.ts +18 -0
- package/dist/proxmox/generated/pbs/ping.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/pull.d.ts +37 -0
- package/dist/proxmox/generated/pbs/pull.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/push.d.ts +36 -0
- package/dist/proxmox/generated/pbs/push.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/reader.d.ts +41 -0
- package/dist/proxmox/generated/pbs/reader.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/root.d.ts +16 -0
- package/dist/proxmox/generated/pbs/root.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/status.d.ts +54 -0
- package/dist/proxmox/generated/pbs/status.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/tape-drive.d.ts +181 -0
- package/dist/proxmox/generated/pbs/tape-drive.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/tape-media.d.ts +126 -0
- package/dist/proxmox/generated/pbs/tape-media.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/tape.d.ts +90 -0
- package/dist/proxmox/generated/pbs/tape.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs/version.d.ts +20 -0
- package/dist/proxmox/generated/pbs/version.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs.d.ts +44 -662
- package/dist/proxmox/generated/pbs.d.ts.map +1 -1
- package/dist/proxmox/generated/pve/access-roles.d.ts +164 -0
- package/dist/proxmox/generated/pve/access-roles.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/access-users.d.ts +152 -0
- package/dist/proxmox/generated/pve/access-users.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/access.d.ts +221 -0
- package/dist/proxmox/generated/pve/access.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-acme-plugins-id.d.ts +45 -0
- package/dist/proxmox/generated/pve/cluster-acme-plugins-id.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-acme-plugins.d.ts +47 -0
- package/dist/proxmox/generated/pve/cluster-acme-plugins.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-acme-tos.d.ts +240 -0
- package/dist/proxmox/generated/pve/cluster-acme-tos.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-bulk-action.d.ts +200 -0
- package/dist/proxmox/generated/pve/cluster-bulk-action.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-firewall-refs.d.ts +109 -0
- package/dist/proxmox/generated/pve/cluster-firewall-refs.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-firewall.d.ts +225 -0
- package/dist/proxmox/generated/pve/cluster-firewall.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-ha.d.ts +199 -0
- package/dist/proxmox/generated/pve/cluster-ha.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-jobs.d.ts +183 -0
- package/dist/proxmox/generated/pve/cluster-jobs.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-metrics.d.ts +110 -0
- package/dist/proxmox/generated/pve/cluster-metrics.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-notifications-endpoints.d.ts +224 -0
- package/dist/proxmox/generated/pve/cluster-notifications-endpoints.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-notifications.d.ts +91 -0
- package/dist/proxmox/generated/pve/cluster-notifications.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-options.d.ts +85 -0
- package/dist/proxmox/generated/pve/cluster-options.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.d.ts +48 -0
- package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models.d.ts +45 -0
- package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-replication.d.ts +130 -0
- package/dist/proxmox/generated/pve/cluster-replication.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts +220 -0
- package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-sdn-ipams.d.ts +229 -0
- package/dist/proxmox/generated/pve/cluster-sdn-ipams.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-sdn-vnets-vnet.d.ts +229 -0
- package/dist/proxmox/generated/pve/cluster-sdn-vnets-vnet.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-sdn-vnets.d.ts +51 -0
- package/dist/proxmox/generated/pve/cluster-sdn-vnets.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-sdn-zones.d.ts +225 -0
- package/dist/proxmox/generated/pve/cluster-sdn-zones.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster-sdn.d.ts +202 -0
- package/dist/proxmox/generated/pve/cluster-sdn.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/cluster.d.ts +69 -0
- package/dist/proxmox/generated/pve/cluster.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-ceph.d.ts +167 -0
- package/dist/proxmox/generated/pve/nodes-node-ceph.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-config.d.ts +43 -0
- package/dist/proxmox/generated/pve/nodes-node-config.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-disks.d.ts +225 -0
- package/dist/proxmox/generated/pve/nodes-node-disks.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-firewall.d.ts +145 -0
- package/dist/proxmox/generated/pve/nodes-node-firewall.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-hardware.d.ts +195 -0
- package/dist/proxmox/generated/pve/nodes-node-hardware.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.d.ts +191 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.d.ts +33 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.d.ts +56 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-resize.d.ts +28 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-resize.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.d.ts +186 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid.d.ts +191 -0
- package/dist/proxmox/generated/pve/nodes-node-lxc-vmid.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-network.d.ts +150 -0
- package/dist/proxmox/generated/pve/nodes-node-network.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-config.d.ts +301 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-config.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.d.ts +170 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.d.ts +144 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.d.ts +34 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.d.ts +137 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-status.d.ts +179 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-status.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid.d.ts +143 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu-vmid.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu.d.ts +158 -0
- package/dist/proxmox/generated/pve/nodes-node-qemu.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-replication.d.ts +187 -0
- package/dist/proxmox/generated/pve/nodes-node-replication.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-services.d.ts +115 -0
- package/dist/proxmox/generated/pve/nodes-node-services.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-storage-storage.d.ts +202 -0
- package/dist/proxmox/generated/pve/nodes-node-storage-storage.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-storage.d.ts +40 -0
- package/dist/proxmox/generated/pve/nodes-node-storage.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-subscription.d.ts +173 -0
- package/dist/proxmox/generated/pve/nodes-node-subscription.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node-vzdump.d.ts +97 -0
- package/dist/proxmox/generated/pve/nodes-node-vzdump.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes-node.d.ts +207 -0
- package/dist/proxmox/generated/pve/nodes-node.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/nodes.d.ts +236 -0
- package/dist/proxmox/generated/pve/nodes.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/pools.d.ts +82 -0
- package/dist/proxmox/generated/pve/pools.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/storage.d.ts +159 -0
- package/dist/proxmox/generated/pve/storage.d.ts.map +1 -0
- package/dist/proxmox/generated/pve/version.d.ts +21 -0
- package/dist/proxmox/generated/pve/version.d.ts.map +1 -0
- package/dist/proxmox/generated/pve.d.ts +69 -4766
- package/dist/proxmox/generated/pve.d.ts.map +1 -1
- package/dist/proxmox/index.js +996 -372
- package/dist/proxmox/index.js.map +48 -32
- package/dist/proxmox/lxc-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/metric-server-form.d.ts +11 -0
- package/dist/proxmox/metric-server-form.d.ts.map +1 -1
- package/dist/proxmox/metric-server.d.ts.map +1 -1
- package/dist/proxmox/network-apply.d.ts.map +1 -1
- package/dist/proxmox/node-network-form.d.ts +15 -0
- package/dist/proxmox/node-network-form.d.ts.map +1 -1
- package/dist/proxmox/node-network.d.ts.map +1 -1
- package/dist/proxmox/notification-target-endpoint.d.ts +26 -0
- package/dist/proxmox/notification-target-endpoint.d.ts.map +1 -0
- package/dist/proxmox/notification-target-form.d.ts +7 -0
- package/dist/proxmox/notification-target-form.d.ts.map +1 -1
- package/dist/proxmox/notification-target.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-target-endpoint.d.ts +29 -0
- package/dist/proxmox/pbs-notification-target-endpoint.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/qemu.d.ts +10 -0
- package/dist/proxmox/qemu.d.ts.map +1 -1
- package/dist/proxmox/resource-guard.d.ts +15 -0
- package/dist/proxmox/resource-guard.d.ts.map +1 -0
- package/dist/proxmox/resource-spec.d.ts +15 -4
- package/dist/proxmox/resource-spec.d.ts.map +1 -1
- package/dist/proxmox/resource.d.ts +8 -0
- package/dist/proxmox/resource.d.ts.map +1 -1
- package/dist/proxmox/sdn-apply.d.ts.map +1 -1
- package/dist/proxmox/sdn-subnet.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool-write.d.ts +11 -0
- package/dist/proxmox/zfs-pool-write.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool.d.ts.map +1 -1
- package/docs/linux-host-measured.md +56 -0
- package/docs/linux-host.md +186 -0
- package/docs/netbox.md +103 -0
- package/package.json +9 -1
- package/src/linux/directory-lifecycle.ts +220 -0
- package/src/linux/directory.test.ts +132 -0
- package/src/linux/directory.ts +71 -0
- package/src/linux/fake-linux-host.ts +197 -0
- package/src/linux/fake-systemd.ts +103 -0
- package/src/linux/index.ts +32 -0
- package/src/linux/providers.ts +26 -0
- package/src/linux/region.test.ts +89 -0
- package/src/linux/region.ts +118 -0
- package/src/linux/remote-file-form.ts +96 -0
- package/src/linux/remote-file-lifecycle.test.ts +191 -0
- package/src/linux/remote-file-lifecycle.ts +161 -0
- package/src/linux/remote-file-plan.ts +154 -0
- package/src/linux/remote-file.ts +88 -0
- package/src/linux/ssh-command.test.ts +140 -0
- package/src/linux/ssh-command.ts +99 -0
- package/src/linux/ssh-runner.ts +188 -0
- package/src/linux/ssh-scripts.ts +105 -0
- package/src/linux/systemctl.ts +145 -0
- package/src/linux/unit-form.ts +183 -0
- package/src/linux/unit-handlers.ts +73 -0
- package/src/linux/unit-lifecycle.test.ts +220 -0
- package/src/linux/unit-lifecycle.ts +193 -0
- package/src/linux/unit-settle.ts +110 -0
- package/src/linux/unit.ts +81 -0
- package/src/netbox/client.test.ts +67 -0
- package/src/netbox/client.ts +146 -0
- package/src/netbox/constraint-guard.ts +57 -0
- package/src/netbox/constraints.test.ts +114 -0
- package/src/netbox/constraints.ts +152 -0
- package/src/netbox/generated/constraints/index.ts +23 -0
- package/src/netbox/generated/constraints/netbox-ipam.ts +32 -0
- package/src/netbox/index.ts +34 -0
- package/src/netbox/prefix-form.test.ts +127 -0
- package/src/netbox/prefix-form.ts +84 -0
- package/src/netbox/prefix.ts +136 -0
- package/src/netbox/resource.ts +208 -0
- package/src/netbox/values.test.ts +66 -0
- package/src/netbox/values.ts +40 -0
- package/src/proxmox/api-token-form.ts +5 -0
- package/src/proxmox/apply-endpoints.ts +34 -0
- package/src/proxmox/ceph-daemon-form.ts +56 -2
- package/src/proxmox/ceph-daemon.ts +10 -21
- package/src/proxmox/ceph-flag.ts +1 -0
- package/src/proxmox/ceph-fs.ts +1 -0
- package/src/proxmox/ceph-osd-write.ts +30 -4
- package/src/proxmox/ceph-pool.ts +10 -0
- package/src/proxmox/constraints-dialect.test.ts +165 -0
- package/src/proxmox/constraints-forms.test.ts +220 -0
- package/src/proxmox/constraints-host-forms.test.ts +132 -0
- package/src/proxmox/constraints-live.test.ts +155 -0
- package/src/proxmox/constraints-node-forms.test.ts +188 -0
- package/src/proxmox/constraints-required.test.ts +172 -0
- package/src/proxmox/constraints.test.ts +8 -63
- package/src/proxmox/constraints.ts +31 -6
- package/src/proxmox/generated/constraints/index.ts +27 -3
- package/src/proxmox/generated/constraints/pbs-config.ts +63 -1
- package/src/proxmox/generated/constraints/pve-access.ts +15 -3
- package/src/proxmox/generated/constraints/pve-cluster-backup.ts +74 -0
- package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +27 -0
- package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +35 -0
- package/src/proxmox/generated/constraints/pve-cluster-ha.ts +55 -0
- package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +59 -0
- package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +80 -0
- package/src/proxmox/generated/constraints/pve-cluster-replication.ts +43 -0
- package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +90 -0
- package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +57 -0
- package/src/proxmox/generated/constraints/pve-nodes-disks.ts +31 -0
- package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +82 -0
- package/src/proxmox/generated/constraints/pve-nodes-network.ts +77 -0
- package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +125 -0
- package/src/proxmox/generated/constraints/pve-pools.ts +6 -1
- package/src/proxmox/generated/constraints/pve-storage.ts +8 -3
- package/src/proxmox/generated/pbs/access-users.ts +137 -0
- package/src/proxmox/generated/pbs/access.ts +232 -0
- package/src/proxmox/generated/pbs/admin-datastore-store.ts +238 -0
- package/src/proxmox/generated/pbs/admin-metrics.ts +147 -0
- package/src/proxmox/generated/pbs/admin.ts +225 -0
- package/src/proxmox/generated/pbs/backup.ts +121 -0
- package/src/proxmox/generated/pbs/config-access-pam.ts +228 -0
- package/src/proxmox/generated/pbs/config-access.ts +228 -0
- package/src/proxmox/generated/pbs/config-datastore.ts +217 -0
- package/src/proxmox/generated/pbs/config-media-pool.ts +232 -0
- package/src/proxmox/generated/pbs/config-notifications-matchers.ts +187 -0
- package/src/proxmox/generated/pbs/config-notifications.ts +221 -0
- package/src/proxmox/generated/pbs/config-remote.ts +192 -0
- package/src/proxmox/generated/pbs/config-sync.ts +167 -0
- package/src/proxmox/generated/pbs/config-tape.ts +226 -0
- package/src/proxmox/generated/pbs/config-verify.ts +87 -0
- package/src/proxmox/generated/pbs/config.ts +135 -0
- package/src/proxmox/generated/pbs/nodes-node-dns.ts +229 -0
- package/src/proxmox/generated/pbs/nodes-node-services.ts +181 -0
- package/src/proxmox/generated/pbs/nodes-node.ts +244 -0
- package/src/proxmox/generated/pbs/nodes.ts +160 -0
- package/src/proxmox/generated/pbs/ping.ts +16 -0
- package/src/proxmox/generated/pbs/pull.ts +37 -0
- package/src/proxmox/generated/pbs/push.ts +36 -0
- package/src/proxmox/generated/pbs/reader.ts +41 -0
- package/src/proxmox/generated/pbs/root.ts +16 -0
- package/src/proxmox/generated/pbs/status.ts +50 -0
- package/src/proxmox/generated/pbs/tape-drive.ts +220 -0
- package/src/proxmox/generated/pbs/tape-media.ts +133 -0
- package/src/proxmox/generated/pbs/tape.ts +92 -0
- package/src/proxmox/generated/pbs/version.ts +16 -0
- package/src/proxmox/generated/pbs.ts +44 -787
- package/src/proxmox/generated/pve/access-roles.ts +168 -0
- package/src/proxmox/generated/pve/access-users.ts +155 -0
- package/src/proxmox/generated/pve/access.ts +217 -0
- package/src/proxmox/generated/pve/cluster-acme-plugins-id.ts +367 -0
- package/src/proxmox/generated/pve/cluster-acme-plugins.ts +366 -0
- package/src/proxmox/generated/pve/cluster-acme-tos.ts +232 -0
- package/src/proxmox/generated/pve/cluster-bulk-action.ts +227 -0
- package/src/proxmox/generated/pve/cluster-firewall-refs.ts +110 -0
- package/src/proxmox/generated/pve/cluster-firewall.ts +236 -0
- package/src/proxmox/generated/pve/cluster-ha.ts +203 -0
- package/src/proxmox/generated/pve/cluster-jobs.ts +190 -0
- package/src/proxmox/generated/pve/cluster-metrics.ts +116 -0
- package/src/proxmox/generated/pve/cluster-notifications-endpoints.ts +244 -0
- package/src/proxmox/generated/pve/cluster-notifications.ts +99 -0
- package/src/proxmox/generated/pve/cluster-options.ts +189 -0
- package/src/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.ts +272 -0
- package/src/proxmox/generated/pve/cluster-qemu-custom-cpu-models.ts +268 -0
- package/src/proxmox/generated/pve/cluster-replication.ts +127 -0
- package/src/proxmox/generated/pve/cluster-sdn-fabrics.ts +218 -0
- package/src/proxmox/generated/pve/cluster-sdn-ipams.ts +229 -0
- package/src/proxmox/generated/pve/cluster-sdn-vnets-vnet.ts +245 -0
- package/src/proxmox/generated/pve/cluster-sdn-vnets.ts +49 -0
- package/src/proxmox/generated/pve/cluster-sdn-zones.ts +224 -0
- package/src/proxmox/generated/pve/cluster-sdn.ts +201 -0
- package/src/proxmox/generated/pve/cluster.ts +71 -0
- package/src/proxmox/generated/pve/nodes-node-ceph.ts +167 -0
- package/src/proxmox/generated/pve/nodes-node-config.ts +56 -0
- package/src/proxmox/generated/pve/nodes-node-disks.ts +237 -0
- package/src/proxmox/generated/pve/nodes-node-firewall.ts +239 -0
- package/src/proxmox/generated/pve/nodes-node-hardware.ts +215 -0
- package/src/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.ts +232 -0
- package/src/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.ts +1059 -0
- package/src/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.ts +49 -0
- package/src/proxmox/generated/pve/nodes-node-lxc-vmid-resize.ts +285 -0
- package/src/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.ts +184 -0
- package/src/proxmox/generated/pve/nodes-node-lxc-vmid.ts +240 -0
- package/src/proxmox/generated/pve/nodes-node-network.ts +234 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid-config.ts +441 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.ts +215 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.ts +145 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.ts +664 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.ts +188 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid-status.ts +233 -0
- package/src/proxmox/generated/pve/nodes-node-qemu-vmid.ts +187 -0
- package/src/proxmox/generated/pve/nodes-node-qemu.ts +204 -0
- package/src/proxmox/generated/pve/nodes-node-replication.ts +187 -0
- package/src/proxmox/generated/pve/nodes-node-services.ts +221 -0
- package/src/proxmox/generated/pve/nodes-node-storage-storage.ts +226 -0
- package/src/proxmox/generated/pve/nodes-node-storage.ts +40 -0
- package/src/proxmox/generated/pve/nodes-node-subscription.ts +168 -0
- package/src/proxmox/generated/pve/nodes-node-vzdump.ts +96 -0
- package/src/proxmox/generated/pve/nodes-node.ts +211 -0
- package/src/proxmox/generated/pve/nodes.ts +235 -0
- package/src/proxmox/generated/pve/pools.ts +78 -0
- package/src/proxmox/generated/pve/storage.ts +213 -0
- package/src/proxmox/generated/pve/version.ts +21 -0
- package/src/proxmox/generated/pve.ts +69 -7403
- package/src/proxmox/lxc-interior.test.ts +17 -1
- package/src/proxmox/lxc-lifecycle.ts +48 -9
- package/src/proxmox/metric-server-form.ts +10 -0
- package/src/proxmox/metric-server.ts +11 -2
- package/src/proxmox/network-apply.ts +4 -0
- package/src/proxmox/node-network-form.ts +19 -0
- package/src/proxmox/node-network.ts +9 -3
- package/src/proxmox/notification-target-endpoint.ts +46 -0
- package/src/proxmox/notification-target-form.ts +11 -0
- package/src/proxmox/notification-target.ts +5 -2
- package/src/proxmox/pbs-notification-target-endpoint.ts +45 -0
- package/src/proxmox/pbs-notification-target-lifecycle.ts +16 -14
- package/src/proxmox/qemu.ts +9 -1
- package/src/proxmox/resource-guard.ts +61 -0
- package/src/proxmox/resource-spec.ts +16 -4
- package/src/proxmox/resource.ts +27 -27
- package/src/proxmox/sdn-apply.ts +4 -0
- package/src/proxmox/sdn-subnet.ts +5 -0
- package/src/proxmox/zfs-pool-write.ts +12 -0
- package/src/proxmox/zfs-pool.ts +2 -1
- package/dist/proxmox/generated/constraints/pve-cluster.d.ts +0 -18
- package/dist/proxmox/generated/constraints/pve-cluster.d.ts.map +0 -1
- package/src/proxmox/generated/constraints/pve-cluster.ts +0 -174
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `systemctl` subcommands the systemd family uses, over a HostRunner, plus the parser for the
|
|
3
|
+
* one that returns data.
|
|
4
|
+
*
|
|
5
|
+
* ★ MEASURED 2026-09-22 on Debian 13, systemd 257 (257.13-1~deb13u1), read-only — nothing was
|
|
6
|
+
* enabled, started or reloaded to write this:
|
|
7
|
+
*
|
|
8
|
+
* - `systemctl show -p <list> <unit>` EXITS 0 FOR A UNIT THAT DOES NOT EXIST, and answers with
|
|
9
|
+
* the properties it would have had:
|
|
10
|
+
* Id=hf-nonexistent-xyz.service LoadState=not-found
|
|
11
|
+
* ActiveState=inactive SubState=dead
|
|
12
|
+
* FragmentPath= UnitFileState=
|
|
13
|
+
* ⛔ SO THE EXIT CODE IS NOT THE ANSWER — `LoadState` is. A provider that read exit 0 as "the
|
|
14
|
+
* unit is there" would adopt a unit that does not exist and then never write its file.
|
|
15
|
+
* - The properties come back in systemd's own order, not the order asked for, so the output is a
|
|
16
|
+
* map and never positional.
|
|
17
|
+
* - `systemctl is-enabled <unknown>` exits **4** and prints `not-found`; `is-active <unknown>`
|
|
18
|
+
* exits **4** and prints `inactive`. Both are answers, not errors.
|
|
19
|
+
* - `UnitFileState=static` is exactly systemd's name for a unit file with no `[Install]` section
|
|
20
|
+
* (measured: the journal daemon is `static`, the ssh daemon is `enabled`).
|
|
21
|
+
* - `/etc/systemd/system` is `root:root 0755` — the admin drop-in directory, ahead of the
|
|
22
|
+
* vendor's `/usr/lib/systemd/system` in systemd's own search order.
|
|
23
|
+
* ⚠️ REASONED, NOT MEASURED: the write subcommands. `daemon-reload`, `enable`, `disable`, `start`,
|
|
24
|
+
* `stop` and `restart` were never run for this file, because running them would change a live
|
|
25
|
+
* host. Each is therefore checked the same way: exit 0 or an Error carrying systemd's stderr, and
|
|
26
|
+
* the state is read back with `show` afterwards rather than assumed.
|
|
27
|
+
* ⛔ NEVER PUT `show` STDOUT WITH NO PROPERTY FILTER IN AN ERROR: the unfiltered property set
|
|
28
|
+
* carries the unit's whole environment block.
|
|
29
|
+
*/
|
|
30
|
+
import type { HostRunner } from '../launchd/runner.ts';
|
|
31
|
+
|
|
32
|
+
export const SYSTEMCTL = 'systemctl';
|
|
33
|
+
|
|
34
|
+
/** The properties every read asks for. ⛔ Never `Environment` — see the header. */
|
|
35
|
+
export const SHOW_PROPERTIES = [
|
|
36
|
+
'Id',
|
|
37
|
+
'LoadState',
|
|
38
|
+
'ActiveState',
|
|
39
|
+
'SubState',
|
|
40
|
+
'UnitFileState',
|
|
41
|
+
'FragmentPath',
|
|
42
|
+
'NeedDaemonReload',
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
export class SystemctlError extends Error {
|
|
46
|
+
constructor(command: string, exitCode: number, stderr: string) {
|
|
47
|
+
super(`systemctl ${command} -> ${String(exitCode)}: ${stderr.trim().slice(0, 300)}`);
|
|
48
|
+
this.name = 'SystemctlError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type UnitStatus = {
|
|
53
|
+
/** `loaded`, `not-found`, `masked`, `error`, `bad-setting`. ⛔ The authority on existence. */
|
|
54
|
+
readonly loadState: string;
|
|
55
|
+
/** systemd knows this unit: it has a unit file or is loaded. */
|
|
56
|
+
readonly known: boolean;
|
|
57
|
+
readonly activeState: string;
|
|
58
|
+
readonly subState?: string;
|
|
59
|
+
/** `enabled`, `disabled`, `static`, `masked`, … Empty for a unit with no unit file. */
|
|
60
|
+
readonly unitFileState?: string;
|
|
61
|
+
/** The unit file systemd would read. Empty when there is none. */
|
|
62
|
+
readonly fragmentPath?: string;
|
|
63
|
+
/** systemd's own view that its on-disk unit files are newer than what it has loaded. */
|
|
64
|
+
readonly needDaemonReload: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** `key=value` lines into a map. ⚠️ First wins: systemd never repeats a key, a truncated read might. */
|
|
68
|
+
export const parseShow = (stdout: string): Map<string, string> => {
|
|
69
|
+
const fields = new Map<string, string>();
|
|
70
|
+
for (const line of stdout.split('\n')) {
|
|
71
|
+
const at = line.indexOf('=');
|
|
72
|
+
if (at <= 0) continue;
|
|
73
|
+
const key = line.slice(0, at);
|
|
74
|
+
if (!fields.has(key)) fields.set(key, line.slice(at + 1));
|
|
75
|
+
}
|
|
76
|
+
return fields;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const statusOf = (stdout: string): UnitStatus => {
|
|
80
|
+
const fields = parseShow(stdout);
|
|
81
|
+
const loadState = fields.get('LoadState') ?? 'not-found';
|
|
82
|
+
const unitFileState = fields.get('UnitFileState') ?? '';
|
|
83
|
+
const fragmentPath = fields.get('FragmentPath') ?? '';
|
|
84
|
+
const subState = fields.get('SubState') ?? '';
|
|
85
|
+
return {
|
|
86
|
+
activeState: fields.get('ActiveState') ?? 'inactive',
|
|
87
|
+
// ⛔ MEASURED: `show` answers for a unit that does not exist, so "known" is LoadState plus
|
|
88
|
+
// whether systemd found a unit file at all — never the exit code.
|
|
89
|
+
known: loadState !== 'not-found' || unitFileState !== '',
|
|
90
|
+
loadState,
|
|
91
|
+
needDaemonReload: fields.get('NeedDaemonReload') === 'yes',
|
|
92
|
+
...(subState === '' ? {} : { subState }),
|
|
93
|
+
...(unitFileState === '' ? {} : { unitFileState }),
|
|
94
|
+
...(fragmentPath === '' ? {} : { fragmentPath }),
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const showUnit = async (runner: HostRunner, name: string): Promise<UnitStatus> => {
|
|
99
|
+
const result = await runner.exec([
|
|
100
|
+
SYSTEMCTL,
|
|
101
|
+
'show',
|
|
102
|
+
'--no-pager',
|
|
103
|
+
'-p',
|
|
104
|
+
SHOW_PROPERTIES.join(','),
|
|
105
|
+
'--',
|
|
106
|
+
name,
|
|
107
|
+
]);
|
|
108
|
+
// ⚠️ A non-zero `show` means systemctl itself failed (no bus, a malformed name) — not a missing
|
|
109
|
+
// unit, which exits 0. Failing closed here is what keeps a broken connection from reading as
|
|
110
|
+
// "the unit is gone", which would make the next deploy write its file and restart it.
|
|
111
|
+
if (result.exitCode !== 0)
|
|
112
|
+
throw new SystemctlError(`show ${name}`, result.exitCode, result.stderr);
|
|
113
|
+
return statusOf(result.stdout);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** One write subcommand, checked. ⚠️ stderr only; `show` output never reaches an error. */
|
|
117
|
+
const run = async (runner: HostRunner, args: readonly string[]): Promise<void> => {
|
|
118
|
+
const result = await runner.exec([SYSTEMCTL, ...args]);
|
|
119
|
+
if (result.exitCode !== 0) {
|
|
120
|
+
throw new SystemctlError(args.join(' '), result.exitCode, result.stderr);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** ⛔ After every unit-file write or removal, and before enable/start reads it. */
|
|
125
|
+
export const daemonReload = (runner: HostRunner): Promise<void> => run(runner, ['daemon-reload']);
|
|
126
|
+
|
|
127
|
+
export const enableUnit = (runner: HostRunner, name: string): Promise<void> =>
|
|
128
|
+
run(runner, ['enable', '--', name]);
|
|
129
|
+
|
|
130
|
+
export const disableUnit = (runner: HostRunner, name: string): Promise<void> =>
|
|
131
|
+
run(runner, ['disable', '--', name]);
|
|
132
|
+
|
|
133
|
+
export const startUnit = (runner: HostRunner, name: string): Promise<void> =>
|
|
134
|
+
run(runner, ['start', '--', name]);
|
|
135
|
+
|
|
136
|
+
export const stopUnit = (runner: HostRunner, name: string): Promise<void> =>
|
|
137
|
+
run(runner, ['stop', '--', name]);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* ⛔ `restart`, NOT `reload-or-restart`. The reloading form asks the unit to re-read its config in
|
|
141
|
+
* place when it declares `ExecReload=`, which means "did the change take effect?" has two
|
|
142
|
+
* different answers depending on the unit — and a deploy cannot report a half-applied change.
|
|
143
|
+
*/
|
|
144
|
+
export const restartUnit = (runner: HostRunner, name: string): Promise<void> =>
|
|
145
|
+
run(runner, ['restart', '--', name]);
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Systemd.Unit / Systemd.Timer props, attributes, the unit-file renderer and the validation — the
|
|
3
|
+
* pure half of the family.
|
|
4
|
+
*
|
|
5
|
+
* ⛔ THE DIRECTIVE SET IS SYSTEMD'S, NOT THIS FILE'S. There is no machine-readable schema of
|
|
6
|
+
* `systemd.unit(5)` / `systemd.service(5)` directives to generate from, so this renders SECTIONS
|
|
7
|
+
* AND LINES VERBATIM and claims to know nothing about what the keys mean. What it validates is
|
|
8
|
+
* only what an INI file must be true of — a section name, a key shape, no embedded newline —
|
|
9
|
+
* plus the two structural rules systemd's own output proves (see `unitProblems`). ★ That is the
|
|
10
|
+
* honest boundary: invent no vendor shapes, and let `systemd-analyze verify` be the deep check a
|
|
11
|
+
* host stack runs on the host.
|
|
12
|
+
* ⛔ `content` IS NEVER A SECRET. Props sit unencrypted in Alchemy state and the unit file is
|
|
13
|
+
* world-readable at 0644; a unit that needs a secret takes `EnvironmentFile=` pointing at a file
|
|
14
|
+
* a secret renderer maintains.
|
|
15
|
+
*/
|
|
16
|
+
import { sha256Hex } from '../launchd/job-form.ts';
|
|
17
|
+
|
|
18
|
+
export type UnitSection = {
|
|
19
|
+
/** `Unit`, `Service`, `Timer`, `Install`, … — verbatim, including its capitalisation. */
|
|
20
|
+
readonly name: string;
|
|
21
|
+
/**
|
|
22
|
+
* The section's lines, in order. A repeated key is allowed and meaningful in systemd
|
|
23
|
+
* (`ExecStartPre=` twice runs twice), so this is a list of pairs, never an object.
|
|
24
|
+
*/
|
|
25
|
+
readonly lines: readonly (readonly [key: string, value: string])[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type SystemdUnitProps = {
|
|
29
|
+
/** Unit name WITH its suffix, e.g. `thing.service`. The unit file's name on disk. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** The whole unit file, verbatim. Mutually exclusive with `sections`. */
|
|
32
|
+
content?: string;
|
|
33
|
+
/** The unit file as sections, rendered in the order given. Mutually exclusive with `content`. */
|
|
34
|
+
sections?: readonly UnitSection[];
|
|
35
|
+
/** Directory the unit file goes in. @default '/etc/systemd/system' */
|
|
36
|
+
directory?: string;
|
|
37
|
+
/** `systemctl enable` / `disable`. @default true */
|
|
38
|
+
enabled?: boolean;
|
|
39
|
+
/** `systemctl start` / `stop`. @default true */
|
|
40
|
+
started?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Digests of things this unit READS that live outside its unit file — a rendered config file's
|
|
43
|
+
* `sha256` attribute, most often. A change here restarts the unit; nothing else does.
|
|
44
|
+
* ⚠️ A hand edit of that file is NOT noticed: the digest in state still matches what the stack
|
|
45
|
+
* declared, so the unit reads converged. That limit is the same one the Mac side records.
|
|
46
|
+
*/
|
|
47
|
+
restartOn?: readonly string[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type SystemdUnitAttributes = {
|
|
51
|
+
name: string;
|
|
52
|
+
unitPath: string;
|
|
53
|
+
/** SHA-256 of the unit file this resource wrote. */
|
|
54
|
+
unitSha256: string;
|
|
55
|
+
/** SHA-256 of `restartOn`, so a config change is a diff. */
|
|
56
|
+
configSha256: string;
|
|
57
|
+
loadState: string;
|
|
58
|
+
activeState: string;
|
|
59
|
+
unitFileState?: string;
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
active: boolean;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** ★ systemd's admin drop-in directory, ahead of the vendor's in its search order (measured). */
|
|
65
|
+
export const DEFAULT_UNIT_DIRECTORY = '/etc/systemd/system';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Directories systemd looks in for a unit file it can enable.
|
|
69
|
+
* ★ MEASURED 2026-09-22 with `systemd-analyze unit-paths` on Debian 13 / systemd 257, minus the
|
|
70
|
+
* per-user and generator directories, which are not places a stack writes. `/lib/systemd/system`
|
|
71
|
+
* is the same directory before the /usr merge, where the measured host has only the merged name.
|
|
72
|
+
* ⛔ WHY IT IS ENFORCED. `systemctl enable <name>` looks the unit up BY NAME; a unit file outside
|
|
73
|
+
* these directories is invisible to that lookup, so an enabled unit declared into, say, `/opt`
|
|
74
|
+
* writes its file, reloads, and then fails at `enable` with the file already on disk.
|
|
75
|
+
*/
|
|
76
|
+
export const UNIT_SEARCH_DIRECTORIES = [
|
|
77
|
+
'/etc/systemd/system',
|
|
78
|
+
'/run/systemd/system',
|
|
79
|
+
'/usr/local/lib/systemd/system',
|
|
80
|
+
'/usr/lib/systemd/system',
|
|
81
|
+
'/lib/systemd/system',
|
|
82
|
+
] as const;
|
|
83
|
+
|
|
84
|
+
/** ⛔ The unit file's mode and owner are systemd's rule, not a choice: readable, never writable. */
|
|
85
|
+
export const UNIT_WRITE = { gid: 0, mode: 0o644, uid: 0 } as const;
|
|
86
|
+
|
|
87
|
+
export const digestOf = (text: string): string => sha256Hex(new TextEncoder().encode(text));
|
|
88
|
+
|
|
89
|
+
/** The digests of everything outside the unit file that should restart it, as one value. */
|
|
90
|
+
export const configDigest = (restartOn: readonly string[] | undefined): string =>
|
|
91
|
+
digestOf((restartOn ?? []).join('\n'));
|
|
92
|
+
|
|
93
|
+
/** Sections → an INI unit file. ★ Keys and values pass through untouched; see the file header. */
|
|
94
|
+
export const renderUnit = (sections: readonly UnitSection[]): string =>
|
|
95
|
+
`${sections
|
|
96
|
+
.map((section) =>
|
|
97
|
+
[`[${section.name}]`, ...section.lines.map(([key, value]) => `${key}=${value}`)].join('\n'),
|
|
98
|
+
)
|
|
99
|
+
.join('\n\n')}\n`;
|
|
100
|
+
|
|
101
|
+
export const unitText = (props: SystemdUnitProps): string =>
|
|
102
|
+
props.content ?? renderUnit(props.sections ?? []);
|
|
103
|
+
|
|
104
|
+
export const unitPathFor = (props: SystemdUnitProps): string =>
|
|
105
|
+
`${props.directory ?? DEFAULT_UNIT_DIRECTORY}/${props.name}`;
|
|
106
|
+
|
|
107
|
+
const NAME = /^[A-Za-z0-9][A-Za-z0-9_.@:-]*\.([a-z]+)$/;
|
|
108
|
+
/** systemd's own unit types. ★ Fixed vocabulary from `systemd.unit(5)`, not a guess at directives. */
|
|
109
|
+
const TYPES = new Set([
|
|
110
|
+
'automount',
|
|
111
|
+
'device',
|
|
112
|
+
'mount',
|
|
113
|
+
'path',
|
|
114
|
+
'scope',
|
|
115
|
+
'service',
|
|
116
|
+
'slice',
|
|
117
|
+
'socket',
|
|
118
|
+
'swap',
|
|
119
|
+
'target',
|
|
120
|
+
'timer',
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
const INSTALL_KEYS = new Set(['WantedBy', 'RequiredBy', 'UpsertedBy', 'Also', 'Alias']);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Every refusal at once, so one plan shows the whole list.
|
|
127
|
+
* ⛔ THE TWO STRUCTURAL RULES, both proven by systemd's own output rather than assumed:
|
|
128
|
+
* - `enabled: true` needs an `[Install]` section. `systemctl enable` has nothing to link without
|
|
129
|
+
* one, and systemd's name for a unit file that has none is `UnitFileState=static` — measured on
|
|
130
|
+
* a live host, where the journal daemon reads `static` and the ssh daemon reads `enabled`.
|
|
131
|
+
* - A `.timer` needs a `[Timer]` section, and a timer's `[Install]` is what arms it at boot.
|
|
132
|
+
*/
|
|
133
|
+
export const unitProblems = (props: SystemdUnitProps, expect?: string): string[] => {
|
|
134
|
+
const found: string[] = [];
|
|
135
|
+
const type = NAME.exec(props.name)?.[1];
|
|
136
|
+
if (type === undefined || !TYPES.has(type)) {
|
|
137
|
+
found.push(
|
|
138
|
+
`name must be <unit>.<type> with a systemd unit type, got ${JSON.stringify(props.name)}`,
|
|
139
|
+
);
|
|
140
|
+
} else if (expect !== undefined && type !== expect) {
|
|
141
|
+
found.push(`this resource declares a .${expect} unit; ${props.name} is a .${type}`);
|
|
142
|
+
}
|
|
143
|
+
if ((props.content === undefined) === (props.sections === undefined)) {
|
|
144
|
+
found.push('declare exactly one of content or sections');
|
|
145
|
+
}
|
|
146
|
+
const directory = props.directory ?? DEFAULT_UNIT_DIRECTORY;
|
|
147
|
+
if (!directory.startsWith('/') || directory.endsWith('/') || directory.includes('/..')) {
|
|
148
|
+
found.push('directory must be an absolute, normalised path with no trailing slash');
|
|
149
|
+
}
|
|
150
|
+
found.push(...textProblems(unitText(props)));
|
|
151
|
+
if (props.enabled !== false && !UNIT_SEARCH_DIRECTORIES.includes(directory as never)) {
|
|
152
|
+
found.push(
|
|
153
|
+
`an enabled unit must live where systemd looks for it by name (${UNIT_SEARCH_DIRECTORIES.join(', ')}); ` +
|
|
154
|
+
`${directory} is not one of them. Move it, or declare enabled: false.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (props.enabled !== false && !hasInstall(props)) {
|
|
158
|
+
found.push(
|
|
159
|
+
'enabled units need an [Install] section with WantedBy/RequiredBy/Also/Alias; without one ' +
|
|
160
|
+
'systemd calls the unit "static" and `systemctl enable` has nothing to link. Add one, or ' +
|
|
161
|
+
'declare enabled: false and let something else pull it in.',
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (type === 'timer' && !unitText(props).includes('[Timer]')) {
|
|
165
|
+
found.push('a .timer needs a [Timer] section');
|
|
166
|
+
}
|
|
167
|
+
return found;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const hasInstall = (props: SystemdUnitProps): boolean => {
|
|
171
|
+
const text = unitText(props);
|
|
172
|
+
if (!text.includes('[Install]')) return false;
|
|
173
|
+
const after = text.slice(text.indexOf('[Install]'));
|
|
174
|
+
return [...INSTALL_KEYS].some((key) => new RegExp(`^${key}=`, 'm').test(after));
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const textProblems = (text: string): string[] => {
|
|
178
|
+
const found: string[] = [];
|
|
179
|
+
if (text.trim() === '') found.push('the unit file is empty');
|
|
180
|
+
if (text.includes('\u0000')) found.push('the unit file contains NUL');
|
|
181
|
+
if (!/^\s*\[[A-Za-z]+]/.test(text)) found.push('a unit file must begin with a [Section]');
|
|
182
|
+
return found;
|
|
183
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four provider handlers `Systemd.Unit` and `Systemd.Timer` share, as plain functions over a
|
|
3
|
+
* HostRunner — so the two Resources differ only in their TYPE and the unit suffix they insist on.
|
|
4
|
+
*
|
|
5
|
+
* ★ WHY NOT ONE SHARED HANDLER OBJECT. `Provider.of` is typed per Resource, so an object built once
|
|
6
|
+
* and passed to both would have to be widened or cast; functions with their arguments spelled out
|
|
7
|
+
* keep both call sites fully checked and still say each rule once.
|
|
8
|
+
*/
|
|
9
|
+
import { Unowned } from 'alchemy/AdoptPolicy';
|
|
10
|
+
import type { Diff } from 'alchemy/Diff';
|
|
11
|
+
import { isResolved } from 'alchemy/Diff';
|
|
12
|
+
import type { Input } from 'alchemy/Input';
|
|
13
|
+
import * as Effect from 'effect/Effect';
|
|
14
|
+
import { lift, resolvedString } from '../launchd/host-effect.ts';
|
|
15
|
+
import type { HostRunner } from '../launchd/runner.ts';
|
|
16
|
+
import { adoptsAtApply } from '../ownership/adopt.ts';
|
|
17
|
+
import { noteUnfinished } from '../ownership/resume.ts';
|
|
18
|
+
import type { SystemdUnitAttributes, SystemdUnitProps } from './unit-form.ts';
|
|
19
|
+
import { deleteUnit, diffUnit, readUnit, reconcileUnit } from './unit-lifecycle.ts';
|
|
20
|
+
|
|
21
|
+
export const readHandler = (
|
|
22
|
+
runner: HostRunner,
|
|
23
|
+
olds: SystemdUnitProps,
|
|
24
|
+
output: SystemdUnitAttributes | undefined,
|
|
25
|
+
): Effect.Effect<SystemdUnitAttributes | undefined, Error> =>
|
|
26
|
+
lift(async () => {
|
|
27
|
+
const found = await readUnit(runner, olds);
|
|
28
|
+
if (found === undefined) return undefined;
|
|
29
|
+
return output === undefined ? Unowned(found) : found;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const diffHandler = (
|
|
33
|
+
runner: HostRunner,
|
|
34
|
+
instanceId: string,
|
|
35
|
+
news: Input<SystemdUnitProps>,
|
|
36
|
+
output: SystemdUnitAttributes | undefined,
|
|
37
|
+
expect?: string,
|
|
38
|
+
): Effect.Effect<Diff | undefined, Error> => {
|
|
39
|
+
// ★ An unfinished generation of our own: `--adopt` may resume it (ownership/resume.ts).
|
|
40
|
+
if (output === undefined) return noteUnfinished(instanceId);
|
|
41
|
+
if (isResolved(news)) return lift(() => diffUnit(runner, news, output, expect));
|
|
42
|
+
/**
|
|
43
|
+
* ⛔ A RENAME MUST BE SEEN EVEN WHILE OTHER PROPS ARE UNRESOLVED. The engine's default for a prop
|
|
44
|
+
* change is `update`, and an update under a new name writes the new unit file and leaves the old
|
|
45
|
+
* unit running with nothing in state to ever remove it. `content` is exactly what is unresolved
|
|
46
|
+
* when a rendered config file's digest is templated into the same deploy.
|
|
47
|
+
*/
|
|
48
|
+
const name = resolvedString(news, 'name');
|
|
49
|
+
return Effect.succeed(
|
|
50
|
+
name !== undefined && name !== output.name
|
|
51
|
+
? { action: 'replace' as const, deleteFirst: true }
|
|
52
|
+
: undefined,
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const reconcileHandler = (
|
|
57
|
+
runner: HostRunner,
|
|
58
|
+
args: {
|
|
59
|
+
fqn: string;
|
|
60
|
+
instanceId: string;
|
|
61
|
+
news: SystemdUnitProps;
|
|
62
|
+
output: SystemdUnitAttributes | undefined;
|
|
63
|
+
},
|
|
64
|
+
expect?: string,
|
|
65
|
+
): Effect.Effect<SystemdUnitAttributes, Error> =>
|
|
66
|
+
Effect.flatMap(adoptsAtApply(args), (adopt) =>
|
|
67
|
+
lift(() => reconcileUnit(runner, args.news, args.output, adopt, expect)),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export const deleteHandler = (
|
|
71
|
+
runner: HostRunner,
|
|
72
|
+
output: SystemdUnitAttributes,
|
|
73
|
+
): Effect.Effect<void, Error> => lift(() => deleteUnit(runner, output));
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The systemd family against the fake host. ⛔ The assertions that matter here are the NEGATIVE
|
|
3
|
+
* ones: an adopted unit that already matches is not restarted, a re-run restarts nothing, and a
|
|
4
|
+
* read that fails writes nothing. An estate rebuild must be able to reach a host holding a vault
|
|
5
|
+
* or a clock without bouncing it.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
import { fakeLinuxHost } from './fake-linux-host.ts';
|
|
9
|
+
import { statusOf } from './systemctl.ts';
|
|
10
|
+
import { renderUnit, unitProblems } from './unit-form.ts';
|
|
11
|
+
import { deleteUnit, diffUnit, readUnit, reconcileUnit } from './unit-lifecycle.ts';
|
|
12
|
+
|
|
13
|
+
const NAME = 'hf-example.service';
|
|
14
|
+
const PATH = `/etc/systemd/system/${NAME}`;
|
|
15
|
+
const body = (exec: string) =>
|
|
16
|
+
renderUnit([
|
|
17
|
+
{ lines: [['Description', 'an example']], name: 'Unit' },
|
|
18
|
+
{
|
|
19
|
+
lines: [
|
|
20
|
+
['ExecStart', exec],
|
|
21
|
+
['Restart', 'on-failure'],
|
|
22
|
+
],
|
|
23
|
+
name: 'Service',
|
|
24
|
+
},
|
|
25
|
+
{ lines: [['WantedBy', 'multi-user.target']], name: 'Install' },
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const props = { content: body('/usr/bin/example'), name: NAME };
|
|
29
|
+
const host = () => fakeLinuxHost({ dirs: { '/etc/systemd/system': 0 } });
|
|
30
|
+
const verbs = (calls: string[][]) =>
|
|
31
|
+
calls.filter((call) => call[0] === 'systemctl').map((call) => call[1]);
|
|
32
|
+
|
|
33
|
+
describe('create', () => {
|
|
34
|
+
test('writes, reloads, enables and starts — in that order', async () => {
|
|
35
|
+
const fake = host();
|
|
36
|
+
const attrs = await reconcileUnit(fake.runner, props, undefined);
|
|
37
|
+
expect(attrs).toMatchObject({ active: true, enabled: true, name: NAME, unitPath: PATH });
|
|
38
|
+
expect(verbs(fake.calls)).toEqual(['show', 'daemon-reload', 'enable', 'start', 'show']);
|
|
39
|
+
expect(fake.files.get(PATH)?.mode).toBe(0o644);
|
|
40
|
+
expect(fake.files.get(PATH)?.uid).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a start that fails removes the file this deploy wrote and says the unit is down', async () => {
|
|
44
|
+
const fake = fakeLinuxHost({ dirs: { '/etc/systemd/system': 0 } });
|
|
45
|
+
const broken = {
|
|
46
|
+
...fake.runner,
|
|
47
|
+
exec: async (argv: readonly string[]) =>
|
|
48
|
+
argv[1] === 'start'
|
|
49
|
+
? { exitCode: 1, stderr: 'Job failed', stdout: '' }
|
|
50
|
+
: fake.runner.exec(argv),
|
|
51
|
+
};
|
|
52
|
+
await expect(reconcileUnit(broken, props, undefined)).rejects.toThrow(/NOT running/);
|
|
53
|
+
expect(fake.files.has(PATH)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('adoption', () => {
|
|
58
|
+
test('a live unit that already matches is adopted without a restart', async () => {
|
|
59
|
+
const fake = host();
|
|
60
|
+
fake.placeUnit(PATH, props.content, { active: true, enabled: true });
|
|
61
|
+
const before = fake.calls.length;
|
|
62
|
+
const attrs = await reconcileUnit(fake.runner, props, undefined, true);
|
|
63
|
+
expect(attrs).toMatchObject({ active: true, enabled: true });
|
|
64
|
+
const after = verbs(fake.calls.slice(before));
|
|
65
|
+
expect(after).toEqual(['show', 'show']);
|
|
66
|
+
expect(after).not.toContain('restart');
|
|
67
|
+
expect(after).not.toContain('daemon-reload');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('and its diff is a noop, so a plan shows nothing to do', async () => {
|
|
71
|
+
const fake = host();
|
|
72
|
+
fake.placeUnit(PATH, props.content, { active: true, enabled: true });
|
|
73
|
+
const output = await reconcileUnit(fake.runner, props, undefined, true);
|
|
74
|
+
expect(await diffUnit(fake.runner, props, output)).toEqual({ action: 'noop' });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('a live unit with other content is refused without --adopt', async () => {
|
|
78
|
+
const fake = host();
|
|
79
|
+
fake.placeUnit(PATH, body('/usr/bin/something-else'), { active: true, enabled: true });
|
|
80
|
+
await expect(reconcileUnit(fake.runner, props, undefined)).rejects.toThrow(/--adopt/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('restart happens exactly when something changed', () => {
|
|
85
|
+
test('a changed unit file restarts once', async () => {
|
|
86
|
+
const fake = host();
|
|
87
|
+
fake.placeUnit(PATH, body('/usr/bin/old'), { active: true, enabled: true });
|
|
88
|
+
const output = await reconcileUnit(fake.runner, props, undefined, true);
|
|
89
|
+
const before = fake.calls.length;
|
|
90
|
+
const next = { ...props, content: body('/usr/bin/new') };
|
|
91
|
+
expect(await diffUnit(fake.runner, next, output)).toEqual({ action: 'update' });
|
|
92
|
+
const updated = await reconcileUnit(fake.runner, next, output);
|
|
93
|
+
const after = verbs(fake.calls.slice(before));
|
|
94
|
+
expect(after.filter((verb) => verb === 'restart')).toHaveLength(1);
|
|
95
|
+
expect(after).not.toContain('stop');
|
|
96
|
+
expect(updated.active).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('a tracked config digest restarts the unit without rewriting its file', async () => {
|
|
100
|
+
const fake = host();
|
|
101
|
+
const first = await reconcileUnit(fake.runner, { ...props, restartOn: ['aaa'] }, undefined);
|
|
102
|
+
const next = { ...props, restartOn: ['bbb'] };
|
|
103
|
+
expect(await diffUnit(fake.runner, next, first)).toEqual({ action: 'update' });
|
|
104
|
+
const before = fake.calls.length;
|
|
105
|
+
await reconcileUnit(fake.runner, next, first);
|
|
106
|
+
const after = fake.calls.slice(before);
|
|
107
|
+
expect(verbs(after)).toContain('restart');
|
|
108
|
+
expect(after.some((call) => call[0] === 'write')).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('a re-run with nothing changed restarts nothing at all', async () => {
|
|
112
|
+
const fake = host();
|
|
113
|
+
const output = await reconcileUnit(fake.runner, props, undefined);
|
|
114
|
+
const before = fake.calls.length;
|
|
115
|
+
await reconcileUnit(fake.runner, props, output);
|
|
116
|
+
const after = verbs(fake.calls.slice(before));
|
|
117
|
+
expect(after).toEqual(['show', 'show']);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('started: false stops a running unit and does not restart it', async () => {
|
|
121
|
+
const fake = host();
|
|
122
|
+
fake.placeUnit(PATH, props.content, { active: true, enabled: true });
|
|
123
|
+
const output = await reconcileUnit(fake.runner, props, undefined, true);
|
|
124
|
+
const stopped = await reconcileUnit(fake.runner, { ...props, started: false }, output);
|
|
125
|
+
expect(stopped.active).toBe(false);
|
|
126
|
+
expect(verbs(fake.calls)).toContain('stop');
|
|
127
|
+
expect(verbs(fake.calls)).not.toContain('restart');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('refusals', () => {
|
|
132
|
+
test('a read that fails refuses and writes nothing', async () => {
|
|
133
|
+
const fake = fakeLinuxHost({ dirs: { '/etc/systemd/system': 0 }, failShow: true });
|
|
134
|
+
await expect(reconcileUnit(fake.runner, props, undefined)).rejects.toThrow(/systemctl show/);
|
|
135
|
+
expect(fake.files.has(PATH)).toBe(false);
|
|
136
|
+
expect(verbs(fake.calls)).not.toContain('daemon-reload');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('a masked unit is someone’s decision, not drift', async () => {
|
|
140
|
+
const fake = host();
|
|
141
|
+
fake.placeUnit(PATH, props.content, { masked: true });
|
|
142
|
+
await expect(reconcileUnit(fake.runner, props, undefined, true)).rejects.toThrow(/is masked/);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('an unprivileged runner refuses before it touches anything', async () => {
|
|
146
|
+
const fake = fakeLinuxHost({ dirs: { '/etc/systemd/system': 0 }, euid: 1001 });
|
|
147
|
+
await expect(reconcileUnit(fake.runner, props, undefined)).rejects.toThrow(/never calls sudo/);
|
|
148
|
+
expect(fake.calls).toHaveLength(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('an enabled unit with no [Install] is refused at validation, not at systemctl', () => {
|
|
152
|
+
const noInstall = {
|
|
153
|
+
content: '[Unit]\nDescription=x\n[Service]\nExecStart=/bin/true\n',
|
|
154
|
+
name: NAME,
|
|
155
|
+
};
|
|
156
|
+
expect(unitProblems(noInstall).join(' ')).toMatch(/\[Install\]/);
|
|
157
|
+
expect(unitProblems({ ...noInstall, enabled: false })).toEqual([]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('a timer resource refuses a .service, and a .timer needs a [Timer] section', () => {
|
|
161
|
+
expect(unitProblems(props, 'timer').join(' ')).toMatch(/is a \.service/);
|
|
162
|
+
expect(unitProblems({ content: props.content, name: 'x.timer' }, 'timer').join(' ')).toMatch(
|
|
163
|
+
/\[Timer\]/,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('an enabled unit outside systemd’s search path is refused at plan time', () => {
|
|
168
|
+
// ⛔ `systemctl enable` looks a unit up BY NAME; a file in /opt is invisible to that lookup,
|
|
169
|
+
// so without this the deploy writes the file, reloads, and only then fails.
|
|
170
|
+
const elsewhere = { ...props, directory: '/opt/app/units' };
|
|
171
|
+
expect(unitProblems(elsewhere).join(' ')).toMatch(/where systemd looks for it by name/);
|
|
172
|
+
expect(unitProblems({ ...elsewhere, enabled: false })).toEqual([]);
|
|
173
|
+
expect(unitProblems({ ...props, directory: '/usr/local/lib/systemd/system' })).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('an enable that systemd did not honour is a refusal, not a claim in state', async () => {
|
|
177
|
+
const fake = host();
|
|
178
|
+
const lying = {
|
|
179
|
+
...fake.runner,
|
|
180
|
+
exec: async (argv: readonly string[]) =>
|
|
181
|
+
argv[1] === 'enable' ? { exitCode: 0, stderr: '', stdout: '' } : fake.runner.exec(argv),
|
|
182
|
+
};
|
|
183
|
+
await expect(reconcileUnit(lying, props, undefined)).rejects.toThrow(/not enabled/);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('both or neither of content and sections is a refusal', () => {
|
|
187
|
+
expect(unitProblems({ name: NAME }).join(' ')).toMatch(/exactly one of content or sections/);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('delete', () => {
|
|
192
|
+
test('stops, disables, removes the file and reloads', async () => {
|
|
193
|
+
const fake = host();
|
|
194
|
+
const output = await reconcileUnit(fake.runner, props, undefined);
|
|
195
|
+
const before = fake.calls.length;
|
|
196
|
+
await deleteUnit(fake.runner, output);
|
|
197
|
+
expect(verbs(fake.calls.slice(before))).toEqual(['show', 'stop', 'disable', 'daemon-reload']);
|
|
198
|
+
expect(fake.files.has(PATH)).toBe(false);
|
|
199
|
+
expect(await readUnit(fake.runner, props)).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('systemctl show, as measured', () => {
|
|
204
|
+
test('a unit that does not exist answers, and the answer is LoadState', () => {
|
|
205
|
+
// MEASURED 2026-09-22, Debian 13 / systemd 257: exit 0, LoadState=not-found.
|
|
206
|
+
const status = statusOf(
|
|
207
|
+
'Id=absent.service\nLoadState=not-found\nActiveState=inactive\nSubState=dead\n' +
|
|
208
|
+
'FragmentPath=\nUnitFileState=\nNeedDaemonReload=no\n',
|
|
209
|
+
);
|
|
210
|
+
expect(status).toMatchObject({ known: false, loadState: 'not-found' });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('a real unit parses whatever order systemd prints', () => {
|
|
214
|
+
const status = statusOf(
|
|
215
|
+
'ExecStart={ path=/usr/sbin/sshd ; argv[]=/usr/sbin/sshd -D $SSHD_OPTS }\n' +
|
|
216
|
+
'Description=OpenBSD Secure Shell server\nLoadState=loaded\nUnitFileState=enabled\n',
|
|
217
|
+
);
|
|
218
|
+
expect(status).toMatchObject({ known: true, loadState: 'loaded', unitFileState: 'enabled' });
|
|
219
|
+
});
|
|
220
|
+
});
|