agent v0.36.0: guest boot-id on /disks (deterministic guest-reboot recreate)
GET /disks emits guest_boot_id = <host-btime>-<guest-init-starttime>: changes on every guest/host boot, stable across controller-only restarts. The controller persists it + deterministically recreates drive-backed apps on change (replaces the timed state-sample). Non-hollow parser test + companion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -222,6 +222,69 @@ func (b *GuestBinder) guestInitPID(ctx context.Context, vmid int) string {
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
// GuestBootID returns a token that CHANGES on every guest boot (host reboot or guest reboot) but is
|
||||
// STABLE across a controller-only restart: "<host-btime>-<guest-init-starttime>". The controller persists
|
||||
// the last-seen value and, when it changes, DETERMINISTICALLY recreates drive-backed apps (they may have
|
||||
// auto-started on the empty stable bind before the agent re-propagated the drive). host-btime (epoch of
|
||||
// the host boot, /proc/stat) changes on a host reboot; the guest init's starttime (field 22 of
|
||||
// /proc/<pid>/stat — ticks since host boot, unique per process launch) changes on a guest reboot. ""
|
||||
// on any read error (the controller then keeps its last-seen → no spurious recreate).
|
||||
func (b *GuestBinder) GuestBootID(ctx context.Context, vmid int) string {
|
||||
pid := b.guestInitPID(ctx, vmid)
|
||||
if pid == "" {
|
||||
return ""
|
||||
}
|
||||
start := procStarttime(pid)
|
||||
if start == "" {
|
||||
return ""
|
||||
}
|
||||
bt := hostBtime()
|
||||
if bt == "" {
|
||||
bt = "0"
|
||||
}
|
||||
return bt + "-" + start
|
||||
}
|
||||
|
||||
// procStarttime returns field 22 (starttime) of /proc/<pid>/stat. The comm field (2) can contain spaces
|
||||
// and parentheses, so we split AFTER the last ')': field 22 is index 19 of the post-comm fields
|
||||
// (field 3 = state is index 0). "" on any error.
|
||||
func procStarttime(pid string) string {
|
||||
data, err := os.ReadFile("/proc/" + pid + "/stat")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return starttimeFromStat(string(data))
|
||||
}
|
||||
|
||||
// starttimeFromStat is the pure parser: field 22 (starttime) of a /proc/<pid>/stat body. The comm field
|
||||
// (2) can contain spaces and parentheses, so split AFTER the LAST ')': field 22 is index 19 of the
|
||||
// post-comm fields (field 3 = state is index 0). "" on a malformed line.
|
||||
func starttimeFromStat(s string) string {
|
||||
rp := strings.LastIndexByte(s, ')')
|
||||
if rp < 0 || rp+2 > len(s) {
|
||||
return ""
|
||||
}
|
||||
fields := strings.Fields(s[rp+1:]) // fields[0] == state (field 3)
|
||||
if len(fields) < 20 {
|
||||
return ""
|
||||
}
|
||||
return fields[19] // field 22 (starttime)
|
||||
}
|
||||
|
||||
// hostBtime returns the host boot time (epoch seconds) from /proc/stat's "btime" line. "" on error.
|
||||
func hostBtime() string {
|
||||
data, err := os.ReadFile("/proc/stat")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if strings.HasPrefix(line, "btime ") {
|
||||
return strings.TrimSpace(strings.TrimPrefix(line, "btime "))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DetachDrive unmounts a drive's felhom-data from the stable parent (propagates OUT of the guest live),
|
||||
// leaving the bare HOST-ROOT-owned stable dir → fail-closed (the guest can't write to it even as root,
|
||||
// since host uid 0 is unmapped). No pct, no reboot. Idempotent: a non-mountpoint is a no-op.
|
||||
|
||||
Reference in New Issue
Block a user