Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/instance/start.go
2611 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package instance
5
6
import (
7
"bufio"
8
"bytes"
9
"context"
10
"errors"
11
"fmt"
12
"os"
13
"os/exec"
14
"path/filepath"
15
"text/template"
16
"time"
17
18
"github.com/docker/go-units"
19
"github.com/lima-vm/go-qcow2reader"
20
"github.com/sirupsen/logrus"
21
22
"github.com/lima-vm/lima/v2/pkg/autostart"
23
"github.com/lima-vm/lima/v2/pkg/cacheutil"
24
"github.com/lima-vm/lima/v2/pkg/driver"
25
"github.com/lima-vm/lima/v2/pkg/driverutil"
26
"github.com/lima-vm/lima/v2/pkg/executil"
27
"github.com/lima-vm/lima/v2/pkg/fileutils"
28
hostagentevents "github.com/lima-vm/lima/v2/pkg/hostagent/events"
29
"github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil"
30
"github.com/lima-vm/lima/v2/pkg/limatype"
31
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
32
"github.com/lima-vm/lima/v2/pkg/registry"
33
"github.com/lima-vm/lima/v2/pkg/store"
34
"github.com/lima-vm/lima/v2/pkg/usrlocal"
35
)
36
37
// DefaultWatchHostAgentEventsTimeout is the duration to wait for the instance
38
// to be running before timing out.
39
const DefaultWatchHostAgentEventsTimeout = 10 * time.Minute
40
41
type Prepared struct {
42
Driver driver.Driver
43
GuestAgent string
44
NerdctlArchiveCache string
45
}
46
47
// Prepare ensures the disk, the nerdctl archive, etc.
48
func Prepare(ctx context.Context, inst *limatype.Instance, guestAgent string) (*Prepared, error) {
49
if !*inst.Config.Plain && guestAgent == "" {
50
var err error
51
guestAgent, err = usrlocal.GuestAgentBinary(*inst.Config.OS, *inst.Config.Arch)
52
if err != nil {
53
return nil, err
54
}
55
}
56
limaDriver, err := driverutil.CreateConfiguredDriver(inst, 0)
57
if err != nil {
58
return nil, fmt.Errorf("failed to create driver instance: %w", err)
59
}
60
61
if err := limaDriver.Validate(ctx); err != nil {
62
return nil, err
63
}
64
65
if err := limaDriver.Create(ctx); err != nil {
66
return nil, err
67
}
68
69
// Check if the instance has been created (the base disk already exists)
70
baseDisk := filepath.Join(inst.Dir, filenames.BaseDisk)
71
_, err = os.Stat(baseDisk)
72
created := err == nil
73
74
kernel := filepath.Join(inst.Dir, filenames.Kernel)
75
kernelCmdline := filepath.Join(inst.Dir, filenames.KernelCmdline)
76
initrd := filepath.Join(inst.Dir, filenames.Initrd)
77
if _, err := os.Stat(baseDisk); errors.Is(err, os.ErrNotExist) {
78
var ensuredBaseDisk bool
79
errs := make([]error, len(inst.Config.Images))
80
for i, f := range inst.Config.Images {
81
if _, err := fileutils.DownloadFile(ctx, baseDisk, f.File, true, "the image", *inst.Config.Arch); err != nil {
82
errs[i] = err
83
continue
84
}
85
if f.Kernel != nil {
86
// ensure decompress kernel because vz expects it to be decompressed
87
if _, err := fileutils.DownloadFile(ctx, kernel, f.Kernel.File, true, "the kernel", *inst.Config.Arch); err != nil {
88
errs[i] = err
89
continue
90
}
91
if f.Kernel.Cmdline != "" {
92
if err := os.WriteFile(kernelCmdline, []byte(f.Kernel.Cmdline), 0o644); err != nil {
93
errs[i] = err
94
continue
95
}
96
}
97
}
98
if f.Initrd != nil {
99
// vz does not need initrd to be decompressed
100
if _, err := fileutils.DownloadFile(ctx, initrd, *f.Initrd, false, "the initrd", *inst.Config.Arch); err != nil {
101
errs[i] = err
102
continue
103
}
104
}
105
ensuredBaseDisk = true
106
break
107
}
108
if !ensuredBaseDisk {
109
return nil, fileutils.Errors(errs)
110
}
111
}
112
113
if err := limaDriver.CreateDisk(ctx); err != nil {
114
return nil, err
115
}
116
117
// Ensure diffDisk size matches the store
118
if err := prepareDiffDisk(ctx, inst); err != nil {
119
return nil, err
120
}
121
122
nerdctlArchiveCache, err := cacheutil.EnsureNerdctlArchiveCache(ctx, inst.Config, created)
123
if err != nil {
124
return nil, err
125
}
126
127
return &Prepared{
128
Driver: limaDriver,
129
GuestAgent: guestAgent,
130
NerdctlArchiveCache: nerdctlArchiveCache,
131
}, nil
132
}
133
134
// StartWithPaths starts the hostagent in the background, which in turn will start the instance.
135
// StartWithPaths will listen to hostagent events and log them to STDOUT until either the instance
136
// is running, or has failed to start.
137
//
138
// The launchHostAgentForeground argument makes the hostagent run in the foreground.
139
// The function will continue to listen and log hostagent events until the instance is
140
// shut down again.
141
//
142
// The showProgress argument tells the hostagent to show provision script progress by tailing cloud-init logs.
143
//
144
// The limactl argument allows the caller to specify the full path of the limactl executable.
145
// The guestAgent argument allows the caller to specify the full path of the guest agent executable.
146
// Inside limactl this function is only called by Start, which passes empty strings for both
147
// limactl and guestAgent, in which case the location of the current executable is used for
148
// limactl and the guest agent is located from the corresponding <prefix>/share/lima directory.
149
//
150
// StartWithPaths calls Prepare by itself, so you do not need to call Prepare manually before calling Start.
151
func StartWithPaths(ctx context.Context, inst *limatype.Instance, launchHostAgentForeground, showProgress bool, limactl, guestAgent string) error {
152
haPIDPath := filepath.Join(inst.Dir, filenames.HostAgentPID)
153
if _, err := os.Stat(haPIDPath); !errors.Is(err, os.ErrNotExist) {
154
return fmt.Errorf("instance %q seems running (hint: remove %q if the instance is not actually running)", inst.Name, haPIDPath)
155
}
156
logrus.Infof("Starting the instance %q with %s VM driver %q", inst.Name, registry.CheckInternalOrExternal(inst.VMType), inst.VMType)
157
158
haSockPath := filepath.Join(inst.Dir, filenames.HostAgentSock)
159
160
prepared, err := Prepare(ctx, inst, guestAgent)
161
if err != nil {
162
return err
163
}
164
165
if limactl == "" {
166
limactl, err = os.Executable()
167
if err != nil {
168
return err
169
}
170
}
171
haStdoutPath := filepath.Join(inst.Dir, filenames.HostAgentStdoutLog)
172
haStderrPath := filepath.Join(inst.Dir, filenames.HostAgentStderrLog)
173
174
begin := time.Now() // used for logrus propagation
175
var haCmd *exec.Cmd
176
if isRegisteredToAutoStart, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {
177
return fmt.Errorf("failed to check autostart registration: %w", err)
178
} else if !isRegisteredToAutoStart || launchHostAgentForeground {
179
if err := os.RemoveAll(haStdoutPath); err != nil {
180
return err
181
}
182
if err := os.RemoveAll(haStderrPath); err != nil {
183
return err
184
}
185
haStdoutW, err := os.Create(haStdoutPath)
186
if err != nil {
187
return err
188
}
189
// no defer haStdoutW.Close()
190
haStderrW, err := os.Create(haStderrPath)
191
if err != nil {
192
return err
193
}
194
// no defer haStderrW.Close()
195
196
var args []string
197
if logrus.GetLevel() >= logrus.DebugLevel {
198
args = append(args, "--debug")
199
}
200
args = append(args,
201
"hostagent",
202
"--pidfile", haPIDPath,
203
"--socket", haSockPath)
204
if prepared.Driver.Info().Features.CanRunGUI {
205
args = append(args, "--run-gui")
206
}
207
if prepared.GuestAgent != "" {
208
args = append(args, "--guestagent", prepared.GuestAgent)
209
}
210
if prepared.NerdctlArchiveCache != "" {
211
args = append(args, "--nerdctl-archive", prepared.NerdctlArchiveCache)
212
}
213
if showProgress {
214
args = append(args, "--progress")
215
}
216
args = append(args, inst.Name)
217
haCmd = exec.CommandContext(ctx, limactl, args...)
218
219
haCmd.SysProcAttr = executil.BackgroundSysProcAttr
220
221
haCmd.Stdout = haStdoutW
222
haCmd.Stderr = haStderrW
223
224
if launchHostAgentForeground {
225
if isRegisteredToAutoStart {
226
logrus.Warn("The instance is registered to start at login, but the --foreground option was given, so starting the instance directly")
227
}
228
haCmd.SysProcAttr = executil.ForegroundSysProcAttr
229
if err := execHostAgentForeground(limactl, haCmd); err != nil {
230
return err
231
}
232
} else if err := haCmd.Start(); err != nil {
233
return err
234
}
235
} else if err = autostart.RequestStart(ctx, inst); err != nil {
236
return fmt.Errorf("failed to request start via autostart manager: %w", err)
237
}
238
239
if err := waitHostAgentStart(ctx, haPIDPath, haStderrPath); err != nil {
240
return err
241
}
242
243
watchErrCh := make(chan error)
244
go func() {
245
watchErrCh <- watchHostAgentEvents(ctx, inst, haStdoutPath, haStderrPath, begin, showProgress)
246
close(watchErrCh)
247
}()
248
waitErrCh := make(chan error)
249
if haCmd != nil {
250
go func() {
251
waitErrCh <- haCmd.Wait()
252
close(waitErrCh)
253
}()
254
} else {
255
defer close(waitErrCh)
256
}
257
258
select {
259
case watchErr := <-watchErrCh:
260
// watchErr can be nil
261
return watchErr
262
// leave the hostagent process running
263
case waitErr := <-waitErrCh:
264
// waitErr should not be nil
265
return fmt.Errorf("host agent process has exited: %w", waitErr)
266
}
267
}
268
269
func Start(ctx context.Context, inst *limatype.Instance, launchHostAgentForeground, showProgress bool) error {
270
return StartWithPaths(ctx, inst, launchHostAgentForeground, showProgress, "", "")
271
}
272
273
func waitHostAgentStart(_ context.Context, haPIDPath, haStderrPath string) error {
274
begin := time.Now()
275
deadlineDuration := 5 * time.Second
276
deadline := begin.Add(deadlineDuration)
277
for {
278
if _, err := os.Stat(haPIDPath); !errors.Is(err, os.ErrNotExist) {
279
return nil
280
}
281
if time.Now().After(deadline) {
282
return fmt.Errorf("hostagent (%q) did not start up in %v (hint: see %q)", haPIDPath, deadlineDuration, haStderrPath)
283
}
284
}
285
}
286
287
func watchHostAgentEvents(ctx context.Context, inst *limatype.Instance, haStdoutPath, haStderrPath string, begin time.Time, showProgress bool) error {
288
ctx, cancel := context.WithTimeout(ctx, watchHostAgentTimeout(ctx))
289
defer cancel()
290
291
var (
292
printedSSHLocalPort bool
293
receivedRunningEvent bool
294
cloudInitCompleted bool
295
err error
296
)
297
298
onEvent := func(ev hostagentevents.Event) bool {
299
if !printedSSHLocalPort && ev.Status.SSHLocalPort != 0 {
300
logrus.Infof("SSH Local Port: %d", ev.Status.SSHLocalPort)
301
printedSSHLocalPort = true
302
303
// Update the instance's SSH port
304
inst.SSHLocalPort = ev.Status.SSHLocalPort
305
}
306
307
if showProgress && ev.Status.CloudInitProgress != nil {
308
progress := ev.Status.CloudInitProgress
309
if progress.Active && progress.LogLine == "" {
310
logrus.Infof("Cloud-init provisioning started...")
311
}
312
313
if progress.LogLine != "" {
314
logrus.Infof("[cloud-init] %s", progress.LogLine)
315
}
316
317
if progress.Completed {
318
cloudInitCompleted = true
319
logrus.Infof("Cloud-init progress monitoring done.")
320
}
321
}
322
if len(ev.Status.Errors) > 0 {
323
logrus.Errorf("%+v", ev.Status.Errors)
324
}
325
if ev.Status.Exiting {
326
err = fmt.Errorf("exiting, status=%+v (hint: see %q)", ev.Status, haStderrPath)
327
return true
328
} else if ev.Status.Running {
329
receivedRunningEvent = true
330
if ev.Status.Degraded {
331
logrus.Warnf("DEGRADED. The VM seems running, but file sharing and port forwarding may not work. (hint: see %q)", haStderrPath)
332
err = fmt.Errorf("degraded, status=%+v", ev.Status)
333
return true
334
}
335
336
if xerr := runAnsibleProvision(ctx, inst); xerr != nil {
337
err = xerr
338
return true
339
}
340
341
if showProgress && !cloudInitCompleted {
342
return false
343
}
344
345
if !isLaunchingShell(ctx) {
346
if *inst.Config.Plain {
347
logrus.Infof("READY. Run `ssh -F %q %s` to open the shell.", inst.SSHConfigFile, inst.Hostname)
348
} else {
349
logrus.Infof("READY. Run `%s` to open the shell.", LimactlShellCmd(inst.Name))
350
}
351
}
352
_ = ShowMessage(inst)
353
err = nil
354
return true
355
}
356
return false
357
}
358
359
if xerr := hostagentevents.Watch(ctx, haStdoutPath, haStderrPath, begin, true, onEvent); xerr != nil {
360
return xerr
361
}
362
363
if err != nil {
364
return err
365
}
366
367
if !receivedRunningEvent {
368
return errors.New("did not receive an event with the \"running\" status")
369
}
370
371
return nil
372
}
373
374
type watchHostAgentEventsTimeoutKey = struct{}
375
376
// WithWatchHostAgentTimeout sets the value of the timeout to use for
377
// watchHostAgentEvents in the given Context.
378
func WithWatchHostAgentTimeout(ctx context.Context, timeout time.Duration) context.Context {
379
return context.WithValue(ctx, watchHostAgentEventsTimeoutKey{}, timeout)
380
}
381
382
type launchingShellKey = struct{}
383
384
// WithLaunchingShell marks the context as launching a shell after start,
385
// suppressing the "READY. Run ... to open the shell" message.
386
func WithLaunchingShell(ctx context.Context) context.Context {
387
return context.WithValue(ctx, launchingShellKey{}, true)
388
}
389
390
// IsLaunchingShell returns whether the launching shell flag is set in the context.
391
func isLaunchingShell(ctx context.Context) bool {
392
v, _ := ctx.Value(launchingShellKey{}).(bool)
393
return v
394
}
395
396
// watchHostAgentTimeout returns the value of the timeout to use for
397
// watchHostAgentEvents contained in the given Context, or its default value.
398
func watchHostAgentTimeout(ctx context.Context) time.Duration {
399
if timeout, ok := ctx.Value(watchHostAgentEventsTimeoutKey{}).(time.Duration); ok {
400
return timeout
401
}
402
return DefaultWatchHostAgentEventsTimeout
403
}
404
405
func LimactlShellCmd(instName string) string {
406
shellCmd := fmt.Sprintf("limactl shell %s", instName)
407
if instName == "default" {
408
shellCmd = "lima"
409
}
410
return shellCmd
411
}
412
413
func ShowMessage(inst *limatype.Instance) error {
414
if inst.Message == "" {
415
return nil
416
}
417
t, err := template.New("message").Parse(inst.Message)
418
if err != nil {
419
return err
420
}
421
data, err := store.AddGlobalFields(inst)
422
if err != nil {
423
return err
424
}
425
var b bytes.Buffer
426
if err := t.Execute(&b, data); err != nil {
427
return err
428
}
429
scanner := bufio.NewScanner(&b)
430
logrus.Infof("Message from the instance %q:", inst.Name)
431
for scanner.Scan() {
432
// Avoid prepending logrus "INFO" header, for ease of copy pasting
433
fmt.Fprintln(logrus.StandardLogger().Out, scanner.Text())
434
}
435
return scanner.Err()
436
}
437
438
// prepareDiffDisk checks the disk size difference between inst.Disk and yaml.Disk.
439
// If there is no diffDisk, return nil (the instance has not been initialized or started yet).
440
func prepareDiffDisk(ctx context.Context, inst *limatype.Instance) error {
441
diffDisk := filepath.Join(inst.Dir, filenames.DiffDisk)
442
443
// Handle the instance initialization
444
_, err := os.Stat(diffDisk)
445
if err != nil {
446
if os.IsNotExist(err) {
447
return nil
448
}
449
return err
450
}
451
452
f, err := os.Open(diffDisk)
453
if err != nil {
454
return err
455
}
456
defer f.Close()
457
458
img, err := qcow2reader.Open(f)
459
if err != nil {
460
return err
461
}
462
463
diskSize := img.Size()
464
465
if inst.Disk == diskSize {
466
return nil
467
}
468
469
logrus.Infof("Resize instance %s's disk from %s to %s", inst.Name, units.BytesSize(float64(diskSize)), units.BytesSize(float64(inst.Disk)))
470
471
if inst.Disk < diskSize {
472
inst.Disk = diskSize
473
return errors.New("diffDisk: Shrinking is currently unavailable")
474
}
475
476
diskUtil := proxyimgutil.NewDiskUtil(ctx)
477
478
err = diskUtil.ResizeDisk(ctx, diffDisk, inst.Disk)
479
if err != nil {
480
return err
481
}
482
483
return nil
484
}
485
486