feat(guests): cap guest resources and gate boot-start (task 0008)
Give the Host-side placement a `limits` field — `memory`, `cpu`, and `tasksMax` — that caps the guest's `container@<name>.service` unit via `MemoryMax`, `CPUQuota`, and `TasksMax`, uncapped by default so an unset limit contributes no key and systemd keeps its default. Add an `autoStart` placement option, on by default, driving the container's boot-start so a Host can define a guest without starting it at boot. Give neogaia's skeleton guest modest demonstrative caps.
This commit was merged in pull request #34.
This commit is contained in:
29
.claude/tasks/0008-guest-resource-limits.md
Normal file
29
.claude/tasks/0008-guest-resource-limits.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
spec: guests
|
||||
blocked-by: 0002-guest-walking-skeleton
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
The Host-side placement that caps a Guest's resources so one misbehaving service cannot starve its Host, plus the boot-start toggle.
|
||||
A Host sets `limits` — `memory`, `cpu`, and `tasksMax` — applied to the guest's unit, uncapped by default.
|
||||
A Host sets `autoStart` to control whether the Guest starts at boot, on by default.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] A Host setting `guests.<path>.limits.memory`, `.cpu`, or `.tasksMax` applies the corresponding cap to the guest's unit.
|
||||
- [x] Each limit is uncapped when unset.
|
||||
- [x] `autoStart` starts the Guest at boot by default, and disabling it leaves the Guest defined but not started at boot.
|
||||
- [x] A Host with a capped Guest builds via `nix flake check`, and the resolved unit caps are verifiable by `nix eval`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The caps map to the guest's own systemd unit, `container@<name>.service`, which the container backend generates.
|
||||
The guest module contributes `serviceConfig.MemoryMax`, `.CPUQuota`, and `.TasksMax`, and the module system merges these with the backend's own `serviceConfig` for that unit.
|
||||
Only set caps appear: a `filterAttrs` drops any limit left null, so an unset limit contributes no key and systemd keeps its uncapped default rather than the module writing an explicit "infinity".
|
||||
- `limits.memory` and `limits.cpu` are strings passed through to systemd verbatim (`2G`, `150%`), since systemd already parses size and percentage forms and re-inventing the parsing here would only narrow what the operator can express.
|
||||
`limits.tasksMax` is a positive int, matching `TasksMax`'s count.
|
||||
- `autoStart` is now a placement option defaulting true, and the container's `autoStart` reads from it directly rather than the previous `mkDefault true`.
|
||||
The backend gates `wantedBy = [ "machines.target" ]` on `autoStart`, so `false` leaves the `container@<name>.service` unit fully defined but out of `machines.target` — startable on demand, not at boot — verified by evaluation.
|
||||
- `neogaia`'s skeleton guest carries modest demonstrative caps (`memory = "1G"`, `cpu = "100%"`, `tasksMax = 512`), following task 0002's precedent of exercising the guest path on this host through its own `nix flake check`.
|
||||
Its `autoStart` is left at the default so 0002's live boot smoke test is unaffected.
|
||||
@@ -46,7 +46,13 @@
|
||||
|
||||
# The walking-skeleton guest, enabled like any module: proves the guest path
|
||||
# end to end through this host's `nix flake check`.
|
||||
# Modest caps keep the skeleton guest from starving the laptop.
|
||||
guests.sample.enable = true;
|
||||
guests.sample.limits = {
|
||||
memory = "1G";
|
||||
cpu = "100%";
|
||||
tasksMax = 512;
|
||||
};
|
||||
|
||||
modules.agents.claude-code.enable = true;
|
||||
modules.agents.tools.gitea-axi.enable = true;
|
||||
|
||||
53
lib.nix
53
lib.nix
@@ -123,6 +123,14 @@ let
|
||||
# below would otherwise resolve silently in the secret's favour.
|
||||
mountCollisions = lib.attrNames (builtins.intersectAttrs userMounts secretMounts);
|
||||
|
||||
# The resource caps the operator places on the guest's unit, dropping any
|
||||
# left unset so systemd keeps its uncapped default for those.
|
||||
limitConfig = lib.filterAttrs (_: v: v != null) {
|
||||
MemoryMax = cfg.limits.memory;
|
||||
CPUQuota = cfg.limits.cpu;
|
||||
TasksMax = cfg.limits.tasksMax;
|
||||
};
|
||||
|
||||
# A networked guest owns its bridged interface through its own networkd, the only stable MAC pin for a nested container.
|
||||
# The interface is eth0, the name a nested container gives its bridged veth.
|
||||
# It takes the placement MAC, and the static address or DHCP when that is unset.
|
||||
@@ -240,6 +248,45 @@ let
|
||||
the host is owned by that same uid inside the guest.
|
||||
'';
|
||||
};
|
||||
limits = {
|
||||
memory = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "2G";
|
||||
description = ''
|
||||
Cap on the guest's memory, applied to its unit as `MemoryMax`.
|
||||
Accepts systemd size suffixes such as `512M` or `2G`. Left null,
|
||||
the guest's memory is uncapped.
|
||||
'';
|
||||
};
|
||||
cpu = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "150%";
|
||||
description = ''
|
||||
Cap on the guest's CPU, applied to its unit as `CPUQuota`, where
|
||||
`100%` is one full core. Left null, the guest's CPU is uncapped.
|
||||
'';
|
||||
};
|
||||
tasksMax = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
example = 512;
|
||||
description = ''
|
||||
Cap on the number of processes and threads the guest may spawn,
|
||||
applied to its unit as `TasksMax`. Left null, the task count is
|
||||
uncapped.
|
||||
'';
|
||||
};
|
||||
};
|
||||
autoStart = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Start the guest at boot. On by default. Disabled, the guest stays
|
||||
defined and can be started on demand, but does not come up at boot.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
@@ -268,8 +315,12 @@ let
|
||||
}
|
||||
];
|
||||
|
||||
# The operator's resource caps land on the guest's own unit.
|
||||
systemd.services."container@${machineName}".serviceConfig =
|
||||
lib.mkIf (cfg.backend == "container") limitConfig;
|
||||
|
||||
containers.${machineName} = lib.mkIf (cfg.backend == "container") {
|
||||
autoStart = lib.mkDefault true;
|
||||
autoStart = cfg.autoStart;
|
||||
|
||||
# The guest gets its own network namespace, so its services — its own
|
||||
# sshd included — never contend with the host's.
|
||||
|
||||
Reference in New Issue
Block a user