Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/hostagent/requirements.go
2610 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package hostagent
5
6
import (
7
"errors"
8
"fmt"
9
"runtime"
10
"strings"
11
"time"
12
13
"github.com/lima-vm/sshocker/pkg/ssh"
14
"github.com/sirupsen/logrus"
15
16
"github.com/lima-vm/lima/v2/pkg/limatype"
17
"github.com/lima-vm/lima/v2/pkg/sshutil"
18
)
19
20
func (a *HostAgent) waitForRequirements(label string, requirements []requirement) error {
21
const (
22
retries = 60
23
sleepDuration = 10 * time.Second
24
)
25
var errs []error
26
27
for i, req := range requirements {
28
retryLoop:
29
for j := range retries {
30
logrus.Infof("Waiting for the %s requirement %d of %d: %q", label, i+1, len(requirements), req.description)
31
err := a.waitForRequirement(req)
32
if err == nil {
33
logrus.Infof("The %s requirement %d of %d is satisfied", label, i+1, len(requirements))
34
break retryLoop
35
}
36
if req.fatal {
37
logrus.Infof("No further %s requirements will be checked", label)
38
errs = append(errs, fmt.Errorf("failed to satisfy the %s requirement %d of %d %q: %s; skipping further checks: %w", label, i+1, len(requirements), req.description, req.debugHint, err))
39
return errors.Join(errs...)
40
}
41
if j == retries-1 {
42
errs = append(errs, fmt.Errorf("failed to satisfy the %s requirement %d of %d %q: %s: %w", label, i+1, len(requirements), req.description, req.debugHint, err))
43
break retryLoop
44
}
45
time.Sleep(sleepDuration)
46
}
47
}
48
return errors.Join(errs...)
49
}
50
51
// prefixExportParam will modify a script to be executed by ssh.ExecuteScript so that it exports
52
// all the variables from /mnt/lima-cidata/param.env before invoking the actual interpreter.
53
//
54
// - The script is executed in user mode, so needs to read the file using `sudo`.
55
//
56
// - `sudo cat param.env | while …; do export …; done` does not work because the piping
57
// creates a subshell, and the exported variables are not visible to the parent process.
58
//
59
// - The `<<<"$string"` redirection is not available on alpine-lima, where /bin/bash is
60
// just a wrapper around busybox ash.
61
//
62
// A script that will start with `#!/usr/bin/env ruby` will be modified to look like this:
63
//
64
// while read -r line; do
65
// [ -n "$line" ] && export "$line"
66
// done<<EOF
67
// $(sudo cat /mnt/lima-cidata/param.env)
68
// EOF
69
// /usr/bin/env ruby
70
//
71
// ssh.ExecuteScript will strip the `#!` prefix from the first line and invoke the
72
// rest of the line as the command. The full script is then passed via STDIN. We use
73
// "$(printf '…')" to be able to use \n as newline escapes, to fit everything on a
74
// single line:
75
//
76
// #!/bin/bash -c "$(printf 'while … done<<EOF\n$(sudo …)\nEOF\n/usr/bin/env ruby')"
77
// #!/usr/bin/env ruby
78
// …
79
//
80
// An earlier implementation used $'…' for quoting, but that isn't supported if the
81
// user switched the default shell to fish.
82
func prefixExportParam(script string) (string, error) {
83
interpreter, err := ssh.ParseScriptInterpreter(script)
84
if err != nil {
85
return "", err
86
}
87
88
// TODO we should have a symbolic constant for `/mnt/lima-cidata`
89
exportParam := `param_env="$(sudo cat /mnt/lima-cidata/param.env)"; while read -r line; do [ -n "$line" ] && export "$line"; done<<EOF\n${param_env}\nEOF\n`
90
91
// double up all '%' characters so we can pass them through unchanged in the format string of printf
92
interpreter = strings.ReplaceAll(interpreter, "%", "%%")
93
exportParam = strings.ReplaceAll(exportParam, "%", "%%")
94
// strings will be interpolated into single-quoted strings, so protect any existing single quotes
95
interpreter = strings.ReplaceAll(interpreter, "'", `'"'"'`)
96
exportParam = strings.ReplaceAll(exportParam, "'", `'"'"'`)
97
return fmt.Sprintf("#!/bin/bash -c \"$(printf '%s%s')\"\n%s", exportParam, interpreter, script), nil
98
}
99
100
func (a *HostAgent) waitForRequirement(r requirement) error {
101
logrus.Debugf("executing script %q", r.description)
102
script, err := prefixExportParam(r.script)
103
if err != nil {
104
return err
105
}
106
sshConfig := a.sshConfig
107
if r.noMaster || runtime.GOOS == "windows" {
108
// Remove ControlMaster, ControlPath, and ControlPersist options,
109
// because Cygwin-based SSH clients do not support multiplexing when executing commands.
110
// References:
111
// https://inbox.sourceware.org/cygwin/[email protected]/T/
112
// https://stackoverflow.com/questions/20959792/is-ssh-controlmaster-with-cygwin-on-windows-actually-possible
113
// By removing these options:
114
// - Avoids execution failures when the control master is not yet available.
115
// - Prevents error messages such as:
116
// > mux_client_request_session: read from master failed: Connection reset by peer
117
// > ControlSocket ....sock already exists, disabling multiplexing
118
// > mm_send_fd: sendmsg(2): Connection reset by peer\\r\\nmux_client_request_session: send fds failed\\r\\n
119
sshConfig = &ssh.SSHConfig{
120
ConfigFile: sshConfig.ConfigFile,
121
Persist: false,
122
AdditionalArgs: sshutil.DisableControlMasterOptsFromSSHArgs(sshConfig.AdditionalArgs),
123
}
124
}
125
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, sshConfig, script, r.description)
126
logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err)
127
if err != nil {
128
return fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err)
129
}
130
return nil
131
}
132
133
type requirement struct {
134
description string
135
script string
136
debugHint string
137
fatal bool
138
noMaster bool
139
}
140
141
func (a *HostAgent) essentialRequirements() []requirement {
142
req := make([]requirement, 0)
143
req = append(req,
144
requirement{
145
description: "ssh",
146
script: `#!/bin/bash
147
true
148
`,
149
debugHint: `Failed to SSH into the guest.
150
Make sure that the YAML field "ssh.localPort" is not used by other processes on the host.
151
If any private key under ~/.ssh is protected with a passphrase, you need to have ssh-agent to be running.
152
`,
153
noMaster: true,
154
})
155
startControlMasterReq := requirement{
156
description: "Explicitly start ssh ControlMaster",
157
script: `#!/bin/bash
158
true
159
`,
160
debugHint: `The persistent ssh ControlMaster should be started immediately.`,
161
}
162
if *a.instConfig.Plain {
163
req = append(req, startControlMasterReq)
164
return req
165
}
166
req = append(req,
167
requirement{
168
description: "user session is ready for ssh",
169
script: `#!/bin/bash
170
set -eux -o pipefail
171
if ! timeout 30s bash -c "until sudo diff -q /run/lima-ssh-ready /mnt/lima-cidata/meta-data 2>/dev/null; do sleep 3; done"; then
172
echo >&2 "not ready to start persistent ssh session"
173
exit 1
174
fi
175
`,
176
debugHint: `The boot sequence will terminate any existing user session after updating
177
/etc/environment to make sure the session includes the new values.
178
Terminating the session will break the persistent SSH tunnel, so
179
it must not be created until the session reset is done.
180
`,
181
noMaster: true,
182
})
183
184
if *a.instConfig.MountType == limatype.REVSSHFS && len(a.instConfig.Mounts) > 0 {
185
req = append(req, requirement{
186
description: "sshfs binary to be installed",
187
script: `#!/bin/bash
188
set -eux -o pipefail
189
if ! timeout 30s bash -c "until command -v sshfs; do sleep 3; done"; then
190
echo >&2 "sshfs is not installed yet"
191
exit 1
192
fi
193
`,
194
debugHint: `The sshfs binary was not installed in the guest.
195
Make sure that you are using an officially supported image.
196
Also see "/var/log/cloud-init-output.log" in the guest.
197
A possible workaround is to run "apt-get install sshfs" in the guest.
198
`,
199
})
200
req = append(req, requirement{
201
description: "fuse to \"allow_other\" as user",
202
script: `#!/bin/bash
203
set -eux -o pipefail
204
if ! timeout 30s bash -c "until sudo grep -q ^user_allow_other /etc/fuse*.conf; do sleep 3; done"; then
205
echo >&2 "/etc/fuse.conf (/etc/fuse3.conf) is not updated to contain \"user_allow_other\""
206
exit 1
207
fi
208
`,
209
debugHint: `Append "user_allow_other" to /etc/fuse.conf (/etc/fuse3.conf) in the guest`,
210
})
211
} else {
212
req = append(req, startControlMasterReq)
213
}
214
return req
215
}
216
217
func (a *HostAgent) optionalRequirements() []requirement {
218
req := make([]requirement, 0)
219
if (*a.instConfig.Containerd.System || *a.instConfig.Containerd.User) && !*a.instConfig.Plain {
220
req = append(req,
221
requirement{
222
description: "systemd must be available",
223
fatal: true,
224
script: `#!/bin/bash
225
set -eux -o pipefail
226
if ! command -v systemctl 2>&1 >/dev/null; then
227
echo >&2 "systemd is not available on this OS"
228
exit 1
229
fi
230
`,
231
debugHint: `systemd is required to run containerd, but does not seem to be available.
232
Make sure that you use an image that supports systemd. If you do not want to run
233
containerd, please make sure that both 'container.system' and 'containerd.user'
234
are set to 'false' in the config file.
235
`,
236
},
237
requirement{
238
description: "containerd binaries to be installed",
239
script: `#!/bin/bash
240
set -eux -o pipefail
241
if ! timeout 30s bash -c "until command -v nerdctl || test -x ` + *a.instConfig.GuestInstallPrefix + `/bin/nerdctl; do sleep 3; done"; then
242
echo >&2 "nerdctl is not installed yet"
243
exit 1
244
fi
245
`,
246
debugHint: `The nerdctl binary was not installed in the guest.
247
Make sure that you are using an officially supported image.
248
Also see "/var/log/cloud-init-output.log" in the guest.
249
`,
250
})
251
}
252
for _, probe := range a.instConfig.Probes {
253
if probe.Mode == limatype.ProbeModeReadiness {
254
req = append(req, requirement{
255
description: probe.Description,
256
script: *probe.Script,
257
debugHint: probe.Hint,
258
})
259
}
260
}
261
return req
262
}
263
264
func (a *HostAgent) finalRequirements() []requirement {
265
req := make([]requirement, 0)
266
req = append(req,
267
requirement{
268
description: "boot scripts must have finished",
269
script: `#!/bin/bash
270
set -eux -o pipefail
271
if ! timeout 30s bash -c "until sudo diff -q /run/lima-boot-done /mnt/lima-cidata/meta-data 2>/dev/null; do sleep 3; done"; then
272
echo >&2 "boot scripts have not finished"
273
exit 1
274
fi
275
`,
276
debugHint: `All boot scripts, provisioning scripts, and readiness probes must
277
finish before the instance is considered "ready".
278
Check "/var/log/cloud-init-output.log" in the guest to see where the process is blocked!
279
`,
280
})
281
return req
282
}
283
284