carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "bytes"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "slices"
9 "strings"
10)
11
12// systemRecoverUnit is carlos-system-recover.service, spelled out once
13// rather than repeated as a string literal at each call site below.
14const systemRecoverUnit = "carlos-system-recover.service"
15
16// instanceControlSocket is the root instance-control channel's socket
17// (design §5.2). ensureSystemUnits arms it the same way it arms the
18// self-update units: this pass is the second belt for any unit a roll
19// script forgot, and a box with the socket unarmed has no isolated-exec
20// control channel at all.
21const instanceControlSocket = "carlos-instance-control.socket"
22
23// ensureSystemArmed is what ensureSystemUnits arms (systemctlPlan: enable,
24// and start for the path unit and the sockets); carlos-dbread.socket only
25// where host.env arms db reads (dbReadsSocketWanted). The claim census reads
26// the same list (claimCensusEnabledUnits), so a unit armed here always has
27// its install links classified there.
28var ensureSystemArmed = []string{systemUpdatePathUnit, systemRecoverUnit, instanceControlSocket, dbReadSocketUnit}
29
30// carlos-dbread.socket and its service template are the db-read worker's
31// (db region forward spec §8.3): installed and the socket armed here, the
32// template never started.
33var systemUnitFiles = []string{"carlos-system-update.path", "carlos-system-update.service", systemRecoverUnit, instanceControlSocket, "carlos-instance-control.service", dbReadSocketUnit, "carlos-dbread@.service"}
34
35// ensureSystemUnits is the second belt of spec §7: a box the roll script
36// forgot to arm is armed within the hour by the root adopt pass. It
37// installs the embedded units when missing and arms them through
38// systemctlPlan (never starting a service). Non-fatal: it runs before
39// RunAdopt's registry and pubkey gates, and logs instead of exiting.
40//
41// It writes into the same <etc>/systemd/system unit files that `carlos
42// system apply`'s installUnits copies, using the same durable-write
43// staging name — so it takes the apply's own lock, non-blocking, before
44// touching anything. An apply in flight wins; this pass leaves it alone
45// rather than risk tearing a unit file mid-copy. lockPath is the apply's
46// lock file (systemRootDir(release.OptDir())/lock in production; a temp
47// path in tests).
48//
49// Two systemctl seams, because the two uses want opposite things from an
50// exit status: systemctl runs the arming COMMANDS, where non-zero is a
51// failure to report, and probe asks the two QUESTIONS, where non-zero is
52// half the answer (see the block below).
53func ensureSystemUnits(etcDir, lockPath string, systemctl, probe func(args ...string) (string, error), logf func(string, ...any)) {
54 if err := os.MkdirAll(filepath.Dir(lockPath), 0o755); err != nil {
55 logf("adopt: system units: %v", err)
56 return
57 }
58 lock, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600)
59 if err != nil {
60 logf("adopt: system units: %v", err)
61 return
62 }
63 defer lock.Close()
64 // Any error here — real contention, or errSystemUnsupported on the
65 // windows build that has no flock — means don't touch the units: an
66 // apply may be mid-copy, or this platform can't tell, and either way
67 // leaving it alone is the safe default. The lock's cadence takes care
68 // of trying again next tick.
69 if err := flockExclusiveNonblock(lock); err != nil {
70 logf("adopt: system units: an apply is running; leaving the self-update units to it")
71 return
72 }
73
74 claimSettings, settingsErr := readClaimUnitSettings(filepath.Join(etcDir, "carlos", "host.env"))
75 if settingsErr != nil {
76 logf("adopt: instance-control unit settings: %v", settingsErr)
77 }
78 unitDir := filepath.Join(etcDir, "systemd", "system")
79 // systemctl answers these two in stdout and spends its EXIT STATUS on
80 // the answer's polarity: is-active exits 3 for "inactive", is-enabled
81 // exits 1 for "disabled" and again for a unit file that is not there
82 // yet. Reading a non-zero exit as a broken systemctl is what made every
83 // unarmed box — the exact box this pass exists to arm — look like a box
84 // with no systemctl at all, so the printed word is the answer and the
85 // exit status is ignored, exactly as probes.go has always done it.
86 active, activeErr := probe("is-active", systemUpdatePathUnit)
87 // The control socket's own is-active check, alongside the self-update
88 // path unit's: the same silent-systemctl-vs-unarmed-unit question, one
89 // more unit wide (design §5.2).
90 ctlActiveOut, ctlActiveErr := probe("is-active", instanceControlSocket)
91 // And the db-read worker's socket (db region forward spec §8.3), which
92 // is armed only where host.env arms db reads: elsewhere it must not
93 // listen at all, so its enablement is asked too.
94 dbrActiveOut, dbrActiveErr := probe("is-active", dbReadSocketUnit)
95 dbrEnabledOut, _ := probe("is-enabled", dbReadSocketUnit)
96 dbrWant, dbrWhy := dbReadsSocketWanted(filepath.Join(etcDir, "carlos", "host.env"))
97 // The is-enabled error is dropped on purpose: its cases are the words
98 // parseIsEnabled reads, and "no such unit file" is a perfectly good
99 // "not enabled" on the box we are here to fix.
100 enabled, _ := probe("is-enabled", systemRecoverUnit)
101 // is-active is the probe that tells a silent systemctl from an unarmed
102 // unit: it prints a state word even for a unit systemd has never heard
103 // of, so no word at all means the question could not be asked — no
104 // binary, or an exec that failed outright.
105 isActive := parseIsActive(active)
106 ctlActive := parseIsActive(ctlActiveOut)
107 dbrActive := parseIsActive(dbrActiveOut)
108 if isActive == nil || ctlActive == nil || dbrActive == nil {
109 logf("adopt: system units: systemctl said nothing; not touching the self-update units: %v / %v / %v", activeErr, ctlActiveErr, dbrActiveErr)
110 return
111 }
112 isEnabled := parseIsEnabled(enabled)
113 missing := false
114 for _, u := range systemUnitFiles {
115 if _, err := os.Stat(filepath.Join(unitDir, u)); err != nil {
116 missing = true
117 }
118 }
119 control, err := unitsFS.ReadFile("units/" + claimControlUnit)
120 if err != nil {
121 logf("adopt: system units: %v", err)
122 return
123 }
124 installed, err := os.ReadFile(filepath.Join(unitDir, claimControlUnit))
125 if err != nil && !os.IsNotExist(err) {
126 logf("adopt: instance-control unit: %v", err)
127 return
128 }
129 controlChanged := settingsErr == nil && (claimSettings.enabled || bytes.Contains(installed, []byte(claimControlEnvironment))) && (err != nil || !bytes.Equal(installed, claimUnitContent(claimControlUnit, control, claimSettings)))
130 dbrEnabled := parseIsEnabled(dbrEnabledOut)
131 // Dark: neither listening nor enabled for the next boot.
132 dbrDark := !*dbrActive && (dbrEnabled == nil || !*dbrEnabled)
133 dbrSettled := (dbrWant && *dbrActive) || (!dbrWant && dbrDark)
134 if !missing && !controlChanged && *isActive && isEnabled != nil && *isEnabled && *ctlActive && dbrSettled {
135 return
136 }
137 if err := os.MkdirAll(unitDir, 0o755); err != nil {
138 logf("adopt: system units: %v", err)
139 return
140 }
141 for _, u := range systemUnitFiles {
142 // A host.env this pass cannot parse says nothing about the claim
143 // (6b-N2): a control unit rendered for an enabled box stays as it is.
144 if u == claimControlUnit && settingsErr != nil && bytes.Contains(installed, []byte(claimControlEnvironment)) {
145 continue
146 }
147 b, err := unitsFS.ReadFile("units/" + u)
148 if err != nil {
149 logf("adopt: system units: %v", err)
150 return
151 }
152 if err := writeFileDurable(filepath.Join(unitDir, u), claimUnitContent(u, b, claimSettings), 0o644); err != nil {
153 logf("adopt: system units: %v", err)
154 return
155 }
156 }
157 arm := ensureSystemArmed
158 if !dbrWant {
159 arm = slices.DeleteFunc(slices.Clone(arm), func(u string) bool { return u == dbReadSocketUnit })
160 if !dbrDark {
161 logf("adopt: system units: db reads are not armed here (%s); stopping %s", dbrWhy, dbReadSocketUnit)
162 if _, err := systemctl("disable", "--now", dbReadSocketUnit); err != nil {
163 logf("adopt: system units: systemctl disable --now %s: %v", dbReadSocketUnit, err)
164 return
165 }
166 }
167 }
168 for _, args := range systemctlPlan(arm) {
169 if _, err := systemctl(args...); err != nil {
170 logf("adopt: system units: systemctl %s: %v", strings.Join(args, " "), err)
171 return
172 }
173 }
174 if settingsErr == nil {
175 if err := restartClaimControl(filepath.Join(unitDir, claimControlUnit), installed, claimUnitContent(claimControlUnit, control, claimSettings), systemctl); err != nil {
176 logf("adopt: system units: %v", err)
177 return
178 }
179 }
180 logf("adopt: self-update units installed and armed")
181}
182
183// parseIsEnabled maps `systemctl is-enabled`'s vocabulary onto "armed for
184// the next boot, or not", as probes.go's parseIsActive maps is-active's onto
185// "running now". enabled and enabled-runtime are the wants-symlink states;
186// static, indirect, alias and generated are units that cannot be enabled at
187// all, so an enable would be a no-op and there is nothing there to fix.
188// Everything else — disabled, masked, linked, the empty answer a unit file
189// that is not installed gets, a word a future systemd invents — is nil, and
190// the caller arms a nil, because the answer this must never invent is the
191// reassuring one.
192func parseIsEnabled(out string) *bool {
193 yes, no := true, false
194 switch strings.TrimSpace(out) {
195 case "enabled", "enabled-runtime", "static", "indirect", "alias", "generated":
196 return &yes
197 case "disabled", "masked", "masked-runtime", "linked", "linked-runtime", "transient", "bad":
198 return &no
199 }
200 return nil
201}
202
203// systemctlProbe is `systemctl <args>` asked as a QUESTION: stdout is the
204// answer and the exit status is the caller's to ignore, so it uses Output()
205// rather than productionSysOps().systemctl's fail-on-non-zero. The error
206// comes back only so a caller left with no word to read can say why.
207func systemctlProbe(args ...string) (string, error) {
208 out, err := exec.Command("systemctl", args...).Output()
209 return string(out), err
210}
211