Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/store/instance.go
2606 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package store
5
6
import (
7
"context"
8
"errors"
9
"fmt"
10
"io"
11
"os"
12
"path/filepath"
13
"runtime"
14
"strconv"
15
"strings"
16
"syscall"
17
"text/tabwriter"
18
"text/template"
19
"time"
20
21
"github.com/docker/go-units"
22
"github.com/sirupsen/logrus"
23
24
"github.com/lima-vm/lima/v2/pkg/driverutil"
25
hostagentclient "github.com/lima-vm/lima/v2/pkg/hostagent/api/client"
26
"github.com/lima-vm/lima/v2/pkg/instance/hostname"
27
"github.com/lima-vm/lima/v2/pkg/limatype"
28
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
29
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
30
"github.com/lima-vm/lima/v2/pkg/textutil"
31
"github.com/lima-vm/lima/v2/pkg/version/versionutil"
32
)
33
34
// Inspect returns err only when the instance does not exist (os.ErrNotExist).
35
// Other errors are returned as *Instance.Errors.
36
func Inspect(ctx context.Context, instName string) (*limatype.Instance, error) {
37
inst := &limatype.Instance{
38
Name: instName,
39
// TODO: support customizing hostname
40
Hostname: hostname.FromInstName(instName),
41
Status: limatype.StatusUnknown,
42
}
43
// InstanceDir validates the instName but does not check whether the instance exists
44
instDir, err := dirnames.InstanceDir(instName)
45
if err != nil {
46
return nil, err
47
}
48
// Make sure inst.Dir is set, even when YAML validation fails
49
inst.Dir = instDir
50
yamlPath := filepath.Join(instDir, filenames.LimaYAML)
51
y, err := LoadYAMLByFilePath(ctx, yamlPath)
52
if err != nil {
53
if errors.Is(err, os.ErrNotExist) {
54
return nil, err
55
}
56
inst.Errors = append(inst.Errors, err)
57
return inst, nil
58
}
59
inst.Config = y
60
inst.Arch = *y.Arch
61
inst.VMType = *y.VMType
62
inst.SSHAddress = "127.0.0.1"
63
inst.SSHLocalPort = *y.SSH.LocalPort // maybe 0
64
inst.SSHConfigFile = filepath.Join(instDir, filenames.SSHConfig)
65
inst.HostAgentPID, err = ReadPIDFile(filepath.Join(instDir, filenames.HostAgentPID))
66
if err != nil {
67
inst.Status = limatype.StatusBroken
68
inst.Errors = append(inst.Errors, err)
69
}
70
71
if inst.HostAgentPID != 0 {
72
haSock := filepath.Join(instDir, filenames.HostAgentSock)
73
haClient, err := hostagentclient.NewHostAgentClient(haSock)
74
if err != nil {
75
inst.Status = limatype.StatusBroken
76
inst.Errors = append(inst.Errors, fmt.Errorf("failed to connect to %q: %w", haSock, err))
77
} else {
78
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
79
defer cancel()
80
info, err := haClient.Info(ctx)
81
if err != nil {
82
inst.Status = limatype.StatusBroken
83
inst.Errors = append(inst.Errors, fmt.Errorf("failed to get Info from %q: %w", haSock, err))
84
} else {
85
inst.SSHLocalPort = info.SSHLocalPort
86
inst.AutoStartedIdentifier = info.AutoStartedIdentifier
87
}
88
}
89
}
90
91
inst.CPUs = *y.CPUs
92
memory, err := units.RAMInBytes(*y.Memory)
93
if err == nil {
94
inst.Memory = memory
95
}
96
disk, err := units.RAMInBytes(*y.Disk)
97
if err == nil {
98
inst.Disk = disk
99
}
100
inst.AdditionalDisks = y.AdditionalDisks
101
inst.Networks = y.Networks
102
103
// 0 out values since not configurable on WSL2
104
if inst.VMType == limatype.WSL2 {
105
inst.Memory = 0
106
inst.CPUs = 0
107
inst.Disk = 0
108
}
109
110
protected := filepath.Join(instDir, filenames.Protected)
111
if _, err := os.Lstat(protected); !errors.Is(err, os.ErrNotExist) {
112
inst.Protected = true
113
}
114
115
inspectStatus(ctx, instDir, inst, y)
116
117
tmpl, err := template.New("format").Parse(y.Message)
118
if err != nil {
119
inst.Errors = append(inst.Errors, fmt.Errorf("message %q is not a valid template: %w", y.Message, err))
120
inst.Status = limatype.StatusBroken
121
} else {
122
data, err := AddGlobalFields(inst)
123
if err != nil {
124
inst.Errors = append(inst.Errors, fmt.Errorf("cannot add global fields to instance data: %w", err))
125
inst.Status = limatype.StatusBroken
126
} else {
127
data.Param = y.Param
128
var message strings.Builder
129
err = tmpl.Execute(&message, data)
130
if err != nil {
131
inst.Errors = append(inst.Errors, fmt.Errorf("cannot execute template %q: %w", y.Message, err))
132
inst.Status = limatype.StatusBroken
133
} else {
134
inst.Message = message.String()
135
}
136
}
137
}
138
139
limaVersionFile := filepath.Join(instDir, filenames.LimaVersion)
140
if version, err := os.ReadFile(limaVersionFile); err == nil {
141
inst.LimaVersion = strings.TrimSpace(string(version))
142
if _, err = versionutil.Parse(inst.LimaVersion); err != nil {
143
logrus.Warnf("treating lima version %q from %q as very latest release", inst.LimaVersion, limaVersionFile)
144
}
145
} else if !errors.Is(err, os.ErrNotExist) {
146
inst.Errors = append(inst.Errors, err)
147
}
148
inst.Param = y.Param
149
return inst, nil
150
}
151
152
func inspectStatus(ctx context.Context, instDir string, inst *limatype.Instance, y *limatype.LimaYAML) {
153
status, err := driverutil.InspectStatus(ctx, inst)
154
if err != nil {
155
inst.Status = limatype.StatusBroken
156
inst.Errors = append(inst.Errors, fmt.Errorf("failed to inspect status: %w", err))
157
return
158
}
159
160
if status == "" {
161
inspectStatusWithPIDFiles(instDir, inst, y)
162
return
163
}
164
165
inst.Status = status
166
}
167
168
func inspectStatusWithPIDFiles(instDir string, inst *limatype.Instance, y *limatype.LimaYAML) {
169
var err error
170
inst.DriverPID, err = ReadPIDFile(filepath.Join(instDir, filenames.PIDFile(*y.VMType)))
171
if err != nil {
172
inst.Status = limatype.StatusBroken
173
inst.Errors = append(inst.Errors, err)
174
}
175
176
if inst.Status == limatype.StatusUnknown {
177
switch {
178
case inst.HostAgentPID > 0 && inst.DriverPID > 0:
179
inst.Status = limatype.StatusRunning
180
case inst.HostAgentPID == 0 && inst.DriverPID == 0:
181
inst.Status = limatype.StatusStopped
182
case inst.HostAgentPID > 0 && inst.DriverPID == 0:
183
inst.Errors = append(inst.Errors, errors.New("host agent is running but driver is not"))
184
inst.Status = limatype.StatusBroken
185
default:
186
inst.Errors = append(inst.Errors, fmt.Errorf("%s driver is running but host agent is not", inst.VMType))
187
inst.Status = limatype.StatusBroken
188
}
189
}
190
}
191
192
// ReadPIDFile returns 0 if the PID file does not exist or the process has already terminated
193
// (in which case the PID file will be removed).
194
func ReadPIDFile(path string) (int, error) {
195
b, err := os.ReadFile(path)
196
if err != nil {
197
if errors.Is(err, os.ErrNotExist) {
198
return 0, nil
199
}
200
return 0, err
201
}
202
pid, err := strconv.Atoi(strings.TrimSpace(string(b)))
203
if err != nil {
204
return 0, err
205
}
206
proc, err := os.FindProcess(pid)
207
if err != nil {
208
return 0, err
209
}
210
// os.FindProcess will only return running processes on Windows, exit early
211
if runtime.GOOS == "windows" {
212
return pid, nil
213
}
214
err = proc.Signal(syscall.Signal(0))
215
if err != nil {
216
if errors.Is(err, os.ErrProcessDone) {
217
_ = os.Remove(path)
218
return 0, nil
219
}
220
// We may not have permission to send the signal (e.g. to network daemon running as root).
221
// But if we get a permissions error, it means the process is still running.
222
if !errors.Is(err, os.ErrPermission) {
223
return 0, err
224
}
225
}
226
return pid, nil
227
}
228
229
type FormatData struct {
230
limatype.Instance `yaml:",inline"`
231
232
// Using these host attributes is deprecated; they will be removed in Lima 3.0
233
// The values are available from `limactl info` as hostOS, hostArch, limaHome, and identifyFile.
234
HostOS string `json:"HostOS" yaml:"HostOS" lima:"deprecated"`
235
HostArch string `json:"HostArch" yaml:"HostArch" lima:"deprecated"`
236
LimaHome string `json:"LimaHome" yaml:"LimaHome" lima:"deprecated"`
237
IdentityFile string `json:"IdentityFile" yaml:"IdentityFile" lima:"deprecated"`
238
}
239
240
var FormatHelp = "\n" +
241
"These functions are available to go templates:\n\n" +
242
textutil.IndentString(2,
243
strings.Join(textutil.FuncHelp, "\n")+"\n")
244
245
func AddGlobalFields(inst *limatype.Instance) (FormatData, error) {
246
var data FormatData
247
data.Instance = *inst
248
// Add HostOS
249
data.HostOS = runtime.GOOS
250
// Add HostArch
251
data.HostArch = limatype.NewArch(runtime.GOARCH)
252
// Add IdentityFile
253
configDir, err := dirnames.LimaConfigDir()
254
if err != nil {
255
return FormatData{}, err
256
}
257
data.IdentityFile = filepath.Join(configDir, filenames.UserPrivateKey)
258
// Add LimaHome
259
data.LimaHome, err = dirnames.LimaDir()
260
if err != nil {
261
return FormatData{}, err
262
}
263
return data, nil
264
}
265
266
type PrintOptions struct {
267
AllFields bool
268
TerminalWidth int
269
}
270
271
// PrintInstances prints instances in a requested format to a given io.Writer.
272
// Supported formats are "json", "yaml", "table", or a go template.
273
func PrintInstances(w io.Writer, instances []*limatype.Instance, format string, options *PrintOptions) error {
274
switch format {
275
case "json":
276
format = "{{json .}}"
277
case "yaml":
278
format = "{{yaml .}}"
279
case "table":
280
types := map[string]int{}
281
archs := map[string]int{}
282
for _, instance := range instances {
283
types[instance.VMType]++
284
archs[instance.Arch]++
285
}
286
all := options != nil && options.AllFields
287
width := 0
288
if options != nil {
289
width = options.TerminalWidth
290
}
291
columnWidth := 8
292
hideType := false
293
hideArch := false
294
hideDir := false
295
296
columns := 1 // NAME
297
columns += 2 // STATUS
298
columns += 2 // SSH
299
// can we still fit the remaining columns (7)
300
if width == 0 || (columns+7)*columnWidth > width && !all {
301
hideType = len(types) == 1
302
}
303
if !hideType {
304
columns++ // VMTYPE
305
}
306
// only hide arch if it is the same as the host arch
307
goarch := limatype.NewArch(runtime.GOARCH)
308
// can we still fit the remaining columns (6)
309
if width == 0 || (columns+6)*columnWidth > width && !all {
310
hideArch = len(archs) == 1 && instances[0].Arch == goarch
311
}
312
if !hideArch {
313
columns++ // ARCH
314
}
315
columns++ // CPUS
316
columns++ // MEMORY
317
columns++ // DISK
318
// can we still fit the remaining columns (2)
319
if width != 0 && (columns+2)*columnWidth > width && !all {
320
hideDir = true
321
}
322
if !hideDir {
323
columns += 2 // DIR
324
}
325
_ = columns
326
327
w := tabwriter.NewWriter(w, 4, 8, 4, ' ', 0)
328
fmt.Fprint(w, "NAME\tSTATUS\tSSH")
329
if !hideType {
330
fmt.Fprint(w, "\tVMTYPE")
331
}
332
if !hideArch {
333
fmt.Fprint(w, "\tARCH")
334
}
335
fmt.Fprint(w, "\tCPUS\tMEMORY\tDISK")
336
if !hideDir {
337
fmt.Fprint(w, "\tDIR")
338
}
339
fmt.Fprintln(w)
340
341
homeDir, err := os.UserHomeDir()
342
if err != nil {
343
return err
344
}
345
346
for _, instance := range instances {
347
dir := instance.Dir
348
if strings.HasPrefix(dir, homeDir) {
349
dir = strings.Replace(dir, homeDir, "~", 1)
350
}
351
fmt.Fprintf(w, "%s\t%s\t%s",
352
instance.Name,
353
instance.Status,
354
fmt.Sprintf("%s:%d", instance.SSHAddress, instance.SSHLocalPort),
355
)
356
if !hideType {
357
fmt.Fprintf(w, "\t%s",
358
instance.VMType,
359
)
360
}
361
if !hideArch {
362
fmt.Fprintf(w, "\t%s",
363
instance.Arch,
364
)
365
}
366
fmt.Fprintf(w, "\t%d\t%s\t%s",
367
instance.CPUs,
368
units.BytesSize(float64(instance.Memory)),
369
units.BytesSize(float64(instance.Disk)),
370
)
371
if !hideDir {
372
fmt.Fprintf(w, "\t%s",
373
dir,
374
)
375
}
376
fmt.Fprint(w, "\n")
377
}
378
return w.Flush()
379
default:
380
// NOP
381
}
382
tmpl, err := template.New("format").Funcs(textutil.TemplateFuncMap).Parse(format)
383
if err != nil {
384
return fmt.Errorf("invalid go template: %w", err)
385
}
386
for _, instance := range instances {
387
data, err := AddGlobalFields(instance)
388
if err != nil {
389
return err
390
}
391
data.Message = strings.TrimSuffix(instance.Message, "\n")
392
err = tmpl.Execute(w, data)
393
if err != nil {
394
return err
395
}
396
fmt.Fprintln(w)
397
}
398
return nil
399
}
400
401