mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:36 +01:00
feat(aquamarine): suspend after idle & wake-on-lan (#503)
This commit is contained in:
1 parent
0888f87075
commit
5c4318b2c9
5 files changed
+152
No files matched your search
@@ -246,6 +246,12 @@ youko mode="switch":
|
|||||||
aquamarine mode="switch":
|
aquamarine mode="switch":
|
||||||
colmena apply {{mode}} --build-on-target --on '@aquamarine' --verbose --show-trace
|
colmena apply {{mode}} --build-on-target --on '@aquamarine' --verbose --show-trace
|
||||||
|
|
||||||
|
# Wake every homelab host that has Wake-on-LAN armed.
|
||||||
|
[linux]
|
||||||
|
[group('homelab')]
|
||||||
|
wake-homelab:
|
||||||
|
python3 {{absolute_path("scripts/wake-homelab.py")}}
|
||||||
|
|
||||||
############################################################################
|
############################################################################
|
||||||
#
|
#
|
||||||
# Commands for other Virtual Machines
|
# Commands for other Virtual Machines
|
||||||
|
|||||||
@@ -289,3 +289,40 @@ just local # on aquamarine itself
|
|||||||
The install only touches the target disk; the staging host keeps booting from its own. To undo, move
|
The install only touches the target disk; the staging host keeps booting from its own. To undo, move
|
||||||
the disk back, restore the `hosts/idols-ai/disko-fs-data.nix` module this change deleted, and
|
the disk back, restore the `hosts/idols-ai/disko-fs-data.nix` module this change deleted, and
|
||||||
re-enroll that machine's TPM2 slot — see `../idols-ai/README.md`.
|
re-enroll that machine's TPM2 slot — see `../idols-ai/README.md`.
|
||||||
|
|
||||||
|
## Wake-on-LAN
|
||||||
|
|
||||||
|
BIOS: `Advanced > Power Setting` must have `Wake By Lan` enabled, and EUP must not cut standby power
|
||||||
|
to the NIC. NixOS arms the RTL8125 through `systemd.network.links` using `OriginalName=en*`.
|
||||||
|
|
||||||
|
After a boot, verify the link reports `Wake-on: g`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ethtool enp133s0 | grep -E 'Supports Wake-on|Wake-on'
|
||||||
|
```
|
||||||
|
|
||||||
|
Wake every homelab host with Wake-on-LAN armed from another L2 host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just wake-homelab
|
||||||
|
```
|
||||||
|
|
||||||
|
The command currently wakes only aquamarine; add a host to `scripts/wake-homelab.py` after its WoL
|
||||||
|
link config and BIOS setting are in place. Test from a powered-off state, not from a reboot.
|
||||||
|
|
||||||
|
## Idle suspend
|
||||||
|
|
||||||
|
`autosuspend` checks every minute and suspends into S3 after 60 minutes without an active user
|
||||||
|
session, meaningful CPU load, or an established connection to one of the request-serving ports:
|
||||||
|
`18080` (ASR), `63081` (llama-swap/Strata), `63083`/`63084` (MagPie), and `65002` (direct Strata).
|
||||||
|
It calls `systemctl suspend`, so systemd sleep inhibitors are also honored. WoL is armed by the
|
||||||
|
`systemd.network.links` configuration above; no separate WoL service is needed.
|
||||||
|
|
||||||
|
The daemon resets its idle timer after resume, so a wake does not immediately go back to sleep. To
|
||||||
|
keep the host awake during maintenance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemd-inhibit --what=sleep --mode=block --who=maintenance --why="manual work" sleep infinity
|
||||||
|
```
|
||||||
|
|
||||||
|
Cancel it with Ctrl-C when finished.
|
||||||
@@ -36,6 +36,7 @@ in
|
|||||||
|
|
||||||
# services
|
# services
|
||||||
./asr.nix
|
./asr.nix
|
||||||
|
./idle-suspend.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
# Bind the board's NCT6798D SuperIO, otherwise fan RPM and board temperatures
|
# Bind the board's NCT6798D SuperIO, otherwise fan RPM and board temperatures
|
||||||
@@ -114,6 +115,13 @@ in
|
|||||||
|
|
||||||
# Predictable names follow the PCIe topology, so match the `en*` prefix instead of
|
# Predictable names follow the PCIe topology, so match the `en*` prefix instead of
|
||||||
# a name; one wired port, so one link.
|
# a name; one wired port, so one link.
|
||||||
|
# `Name=` is not a valid [Match] key in systemd.link; systemd 262 ignores
|
||||||
|
# the whole file with "Unknown key 'Name'". Match the kernel name instead.
|
||||||
|
systemd.network.links."40-wake-on-lan" = {
|
||||||
|
matchConfig.OriginalName = "en*";
|
||||||
|
linkConfig.WakeOnLan = "magic";
|
||||||
|
};
|
||||||
|
|
||||||
systemd.network.networks."10-lan" = {
|
systemd.network.networks."10-lan" = {
|
||||||
matchConfig.Name = [ "en*" ];
|
matchConfig.Name = [ "en*" ];
|
||||||
networkConfig = {
|
networkConfig = {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
myvars,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
idleMinutes = 60;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
services.autosuspend = {
|
||||||
|
enable = true;
|
||||||
|
# nixpkgs' pytest hook misses docutils for test_ext_autosuspend.py.
|
||||||
|
package =
|
||||||
|
(pkgs.autosuspend.override {
|
||||||
|
withDocs = false;
|
||||||
|
withMan = false;
|
||||||
|
}).overridePythonAttrs
|
||||||
|
(old: {
|
||||||
|
nativeCheckInputs =
|
||||||
|
(old.nativeCheckInputs or [ ])
|
||||||
|
++ (with pkgs.python3Packages; [
|
||||||
|
sphinx
|
||||||
|
docutils
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
settings = {
|
||||||
|
interval = 60;
|
||||||
|
idle_time = idleMinutes * 60;
|
||||||
|
suspend_cmd = "${pkgs.systemd}/bin/systemctl suspend";
|
||||||
|
};
|
||||||
|
checks = {
|
||||||
|
Sessions = {
|
||||||
|
class = "LogindSessionsIdle";
|
||||||
|
};
|
||||||
|
Load = {
|
||||||
|
threshold = 1.0;
|
||||||
|
};
|
||||||
|
# ASR listens on 18080.
|
||||||
|
Asr = {
|
||||||
|
class = "ActiveConnection";
|
||||||
|
ports = toString myvars.ports.aquamarineAsr;
|
||||||
|
};
|
||||||
|
# llama-swap/Strata: 63081 is the proxy, 65002 the direct backend.
|
||||||
|
Inference = {
|
||||||
|
class = "ActiveConnection";
|
||||||
|
ports = "63081,65002";
|
||||||
|
};
|
||||||
|
# MagPie API and web UI: 63083 and 63084.
|
||||||
|
MagPie = {
|
||||||
|
class = "ActiveConnection";
|
||||||
|
ports = "63083,63084";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Wake every homelab host whose NixOS config arms Wake-on-LAN."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Keep this list limited to hosts that actually set Wake-on: g in NixOS.
|
||||||
|
# Add a host here only after its WoL service and BIOS setting are in place.
|
||||||
|
HOSTS = {
|
||||||
|
"aquamarine": "8c:32:23:36:81:84",
|
||||||
|
}
|
||||||
|
BROADCASTS = ("192.168.5.255", "255.255.255.255")
|
||||||
|
PORT = 9
|
||||||
|
PACKETS = 3
|
||||||
|
|
||||||
|
|
||||||
|
def magic_packet(mac: str) -> bytes:
|
||||||
|
octets = bytes.fromhex(mac.replace(":", ""))
|
||||||
|
if len(octets) != 6:
|
||||||
|
raise ValueError(f"invalid MAC: {mac}")
|
||||||
|
return b"\xff" * 6 + octets * 16
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for host, mac in HOSTS.items():
|
||||||
|
packet = magic_packet(mac)
|
||||||
|
print(f"waking {host} ({mac})")
|
||||||
|
for _ in range(PACKETS):
|
||||||
|
for broadcast in BROADCASTS:
|
||||||
|
try:
|
||||||
|
sock.sendto(packet, (broadcast, PORT))
|
||||||
|
except OSError as exc:
|
||||||
|
print(f" {broadcast}:{PORT}: {exc}", file=sys.stderr)
|
||||||
|
failed = True
|
||||||
|
|
||||||
|
return 1 if failed else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in new issue
Block a user