Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/cmd/limactl/editflags/editflags.go
2614 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package editflags
5
6
import (
7
"errors"
8
"fmt"
9
"math/bits"
10
"runtime"
11
"slices"
12
"strconv"
13
"strings"
14
15
"github.com/pbnjay/memory"
16
"github.com/sirupsen/logrus"
17
"github.com/spf13/cobra"
18
flag "github.com/spf13/pflag"
19
20
"github.com/lima-vm/lima/v2/pkg/localpathutil"
21
"github.com/lima-vm/lima/v2/pkg/registry"
22
)
23
24
// RegisterEdit registers flags related to in-place YAML modification, for `limactl edit`.
25
func RegisterEdit(cmd *cobra.Command, commentPrefix string) {
26
flags := cmd.Flags()
27
28
flags.Int("cpus", 0, commentPrefix+"Number of CPUs") // Similar to colima's --cpu, but the flag name is slightly different (cpu vs cpus)
29
_ = cmd.RegisterFlagCompletionFunc("cpus", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
30
var res []string
31
for _, f := range completeCPUs(runtime.NumCPU()) {
32
res = append(res, strconv.Itoa(f))
33
}
34
return res, cobra.ShellCompDirectiveNoFileComp
35
})
36
37
flags.IPSlice("dns", nil, commentPrefix+"Specify custom DNS (disable host resolver)") // colima-compatible
38
39
flags.Float32("memory", 0, commentPrefix+"Memory in GiB") // colima-compatible
40
_ = cmd.RegisterFlagCompletionFunc("memory", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
41
var res []string
42
for _, f := range completeMemoryGiB(memory.TotalMemory()) {
43
res = append(res, fmt.Sprintf("%.1f", f))
44
}
45
return res, cobra.ShellCompDirectiveNoFileComp
46
})
47
48
flags.StringSlice("mount", nil, commentPrefix+"Directories to mount, suffix ':w' for writable (Do not specify directories that overlap with the existing mounts)") // colima-compatible
49
flags.StringSlice("mount-only", nil, commentPrefix+"Similar to --mount, but overrides the existing mounts")
50
51
flags.Bool("mount-none", false, commentPrefix+"Remove all mounts")
52
53
flags.String("mount-type", "", commentPrefix+"Mount type (reverse-sshfs, 9p, virtiofs)") // Similar to colima's --mount-type=(sshfs|9p|virtiofs), but "reverse-sshfs" is Lima is called "sshfs" in colima
54
_ = cmd.RegisterFlagCompletionFunc("mount-type", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
55
return []string{"reverse-sshfs", "9p", "virtiofs"}, cobra.ShellCompDirectiveNoFileComp
56
})
57
58
flags.Bool("mount-writable", false, commentPrefix+"Make all mounts writable")
59
flags.Bool("mount-inotify", false, commentPrefix+"Enable inotify for mounts")
60
61
flags.StringSlice("network", nil, commentPrefix+"Additional networks, e.g., \"vzNAT\" or \"lima:shared\" to assign vmnet IP")
62
_ = cmd.RegisterFlagCompletionFunc("network", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
63
// TODO: retrieve the lima:* network list from networks.yaml
64
return []string{"lima:shared", "lima:bridged", "lima:host", "lima:user-v2", "vzNAT"}, cobra.ShellCompDirectiveNoFileComp
65
})
66
67
flags.Bool("rosetta", false, commentPrefix+"Enable Rosetta (for vz instances)")
68
69
flags.StringArray("set", []string{}, commentPrefix+"Modify the template inplace, using yq syntax. Can be passed multiple times.")
70
71
flags.Uint16("ssh-port", 0, commentPrefix+"SSH port (0 for random)") // colima-compatible
72
_ = cmd.RegisterFlagCompletionFunc("ssh-port", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
73
// Until Lima v2.0, 60022 was the default SSH port for the instance named "default".
74
return []string{"60022"}, cobra.ShellCompDirectiveNoFileComp
75
})
76
77
// negative performance impact: https://gitlab.com/qemu-project/qemu/-/issues/334
78
flags.Bool("video", false, commentPrefix+"Enable video output (has negative performance impact for QEMU)")
79
80
flags.Float32("disk", 0, commentPrefix+"Disk size in GiB") // colima-compatible
81
_ = cmd.RegisterFlagCompletionFunc("disk", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
82
return []string{"10", "30", "50", "100", "200"}, cobra.ShellCompDirectiveNoFileComp
83
})
84
85
flags.String("vm-type", "", commentPrefix+"Virtual machine type")
86
_ = cmd.RegisterFlagCompletionFunc("vm-type", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
87
var drivers []string
88
for k := range registry.List() {
89
drivers = append(drivers, k)
90
}
91
return drivers, cobra.ShellCompDirectiveNoFileComp
92
})
93
}
94
95
// RegisterCreate registers flags related to in-place YAML modification, for `limactl create`.
96
func RegisterCreate(cmd *cobra.Command, commentPrefix string) {
97
RegisterEdit(cmd, commentPrefix)
98
flags := cmd.Flags()
99
100
flags.String("arch", "", commentPrefix+"Machine architecture (x86_64, aarch64, riscv64, armv7l, s390x, ppc64le)") // colima-compatible
101
_ = cmd.RegisterFlagCompletionFunc("arch", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
102
return []string{"x86_64", "aarch64", "riscv64", "armv7l", "s390x", "ppc64le"}, cobra.ShellCompDirectiveNoFileComp
103
})
104
105
flags.String("containerd", "", commentPrefix+"containerd mode (user, system, user+system, none)")
106
_ = cmd.RegisterFlagCompletionFunc("containerd", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
107
return []string{"user", "system", "user+system", "none"}, cobra.ShellCompDirectiveNoFileComp
108
})
109
110
flags.Bool("plain", false, commentPrefix+"Plain mode. Disables mounts, port forwarding, containerd, etc.")
111
112
flags.StringArray("port-forward", nil, commentPrefix+"Port forwards (host:guest), e.g., '8080:80' or '9090:9090,static=true' for static port-forwards")
113
_ = cmd.RegisterFlagCompletionFunc("port-forward", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
114
return []string{"8080:80", "3000:3000", "8080:80,static=true"}, cobra.ShellCompDirectiveNoFileComp
115
})
116
}
117
118
func defaultExprFunc(expr string) func(v *flag.Flag) ([]string, error) {
119
return func(v *flag.Flag) ([]string, error) {
120
return []string{fmt.Sprintf(expr, v.Value)}, nil
121
}
122
}
123
124
func ParsePortForward(spec string) (hostPort, guestPort string, isStatic bool, err error) {
125
parts := strings.Split(spec, ",")
126
if len(parts) > 2 {
127
return "", "", false, fmt.Errorf("invalid port forward format %q, expected HOST:GUEST or HOST:GUEST,static=true", spec)
128
}
129
130
portParts := strings.Split(strings.TrimSpace(parts[0]), ":")
131
if len(portParts) != 2 {
132
return "", "", false, fmt.Errorf("invalid port forward format %q, expected HOST:GUEST", parts[0])
133
}
134
135
hostPort = strings.TrimSpace(portParts[0])
136
guestPort = strings.TrimSpace(portParts[1])
137
138
if len(parts) == 2 {
139
staticPart := strings.TrimSpace(parts[1])
140
if staticValue, ok := strings.CutPrefix(staticPart, "static="); ok {
141
isStatic, err = strconv.ParseBool(staticValue)
142
if err != nil {
143
return "", "", false, fmt.Errorf("invalid value for static parameter: %q", staticValue)
144
}
145
} else {
146
return "", "", false, fmt.Errorf("invalid parameter %q, expected 'static=' followed by a boolean value", staticPart)
147
}
148
}
149
150
return hostPort, guestPort, isStatic, nil
151
}
152
153
func BuildPortForwardExpression(portForwards []string) (string, error) {
154
if len(portForwards) == 0 {
155
return "", nil
156
}
157
158
ports := make([]string, len(portForwards))
159
for i, spec := range portForwards {
160
hostPort, guestPort, isStatic, err := ParsePortForward(spec)
161
if err != nil {
162
return "", err
163
}
164
ports[i] = fmt.Sprintf(`{"guestPort": %q, "hostPort": %q, "static": %v}`, guestPort, hostPort, isStatic)
165
}
166
expr := fmt.Sprintf(".portForwards += [%s]", strings.Join(ports, ","))
167
return expr, nil
168
}
169
170
func buildMountListExpression(ss []string) (string, error) {
171
mounts := make([]string, len(ss))
172
for i, s := range ss {
173
writable := strings.HasSuffix(s, ":w")
174
loc, err := localpathutil.Expand(strings.TrimSuffix(s, ":w"))
175
if err != nil {
176
return "", err
177
}
178
mounts[i] = fmt.Sprintf(`{"location": %q, "mountPoint": %q, "writable": %v}`, loc, loc, writable)
179
}
180
expr := fmt.Sprintf("[%s]", strings.Join(mounts, ","))
181
return expr, nil
182
}
183
184
// YQExpressions returns YQ expressions.
185
func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
186
type def struct {
187
flagName string
188
exprFunc func(*flag.Flag) ([]string, error)
189
onlyValidForNewInstances bool
190
experimental bool
191
}
192
d := defaultExprFunc
193
defs := []def{
194
{
195
"cpus",
196
func(_ *flag.Flag) ([]string, error) {
197
numCpus, err := flags.GetInt("cpus")
198
if err != nil {
199
return nil, err
200
}
201
if numCpus < 0 {
202
return nil, errors.New("invalid value for number of cpus, must be >= 0")
203
}
204
return []string{fmt.Sprintf(".cpus = %d", numCpus)}, nil
205
},
206
false,
207
false,
208
},
209
{
210
"dns",
211
func(_ *flag.Flag) ([]string, error) {
212
ipSlice, err := flags.GetIPSlice("dns")
213
if err != nil {
214
return nil, err
215
}
216
ips := make([]string, len(ipSlice))
217
for i, ip := range ipSlice {
218
ips[i] = `"` + ip.String() + `"`
219
}
220
expr := fmt.Sprintf(".dns += [%s] | .dns |= unique | .hostResolver.enabled=false", strings.Join(ips, ","))
221
logrus.Warnf("Disabling HostResolver, as custom DNS addresses (%v) are specified", ipSlice)
222
return []string{expr}, nil
223
},
224
false,
225
false,
226
},
227
{"memory", d(".memory = \"%sGiB\""), false, false},
228
{
229
"mount",
230
func(_ *flag.Flag) ([]string, error) {
231
ss, err := flags.GetStringSlice("mount")
232
slices.Reverse(ss)
233
if err != nil {
234
return nil, err
235
}
236
mountListExpr, err := buildMountListExpression(ss)
237
if err != nil {
238
return nil, err
239
}
240
// mount options take precedence over template settings
241
expr := fmt.Sprintf(".mounts = %s + .mounts", mountListExpr)
242
mountOnly, err := flags.GetStringSlice("mount-only")
243
if err != nil {
244
return nil, err
245
}
246
if len(mountOnly) > 0 {
247
return nil, errors.New("flag `--mount` conflicts with `--mount-only`")
248
}
249
return []string{expr}, nil
250
},
251
false,
252
false,
253
},
254
{
255
"mount-only",
256
func(_ *flag.Flag) ([]string, error) {
257
ss, err := flags.GetStringSlice("mount-only")
258
if err != nil {
259
return nil, err
260
}
261
mountListExpr, err := buildMountListExpression(ss)
262
if err != nil {
263
return nil, err
264
}
265
expr := `.mounts = ` + mountListExpr
266
return []string{expr}, nil
267
},
268
false,
269
false,
270
},
271
{
272
"mount-none",
273
func(_ *flag.Flag) ([]string, error) {
274
incompatibleFlagNames := []string{"mount", "mount-only"}
275
for _, name := range incompatibleFlagNames {
276
ss, err := flags.GetStringSlice(name)
277
if err != nil {
278
return nil, err
279
}
280
if len(ss) > 0 {
281
return nil, errors.New("flag `--mount-none` conflicts with `" + name + "`")
282
}
283
}
284
return []string{".mounts = null"}, nil
285
},
286
false,
287
false,
288
},
289
{"mount-type", d(".mountType = %q"), false, false},
290
{"vm-type", d(".vmType = %q"), false, false},
291
{"mount-inotify", d(".mountInotify = %s"), false, true},
292
{"mount-writable", d(".mounts[].writable = %s"), false, false},
293
{
294
"network",
295
func(_ *flag.Flag) ([]string, error) {
296
ss, err := flags.GetStringSlice("network")
297
if err != nil {
298
return nil, err
299
}
300
networks := make([]string, len(ss))
301
for i, s := range ss {
302
// CLI syntax is still experimental (YAML syntax is out of experimental)
303
switch {
304
case s == "vzNAT":
305
networks[i] = `{"vzNAT": true}`
306
case strings.HasPrefix(s, "lima:"):
307
network := strings.TrimPrefix(s, "lima:")
308
networks[i] = fmt.Sprintf(`{"lima": %q}`, network)
309
default:
310
return nil, fmt.Errorf(`network name must be "vzNAT" or "lima:*", got %q`, s)
311
}
312
}
313
expr := fmt.Sprintf(`.networks += [%s] | .networks |= unique_by(.lima)`, strings.Join(networks, ","))
314
return []string{expr}, nil
315
},
316
false,
317
false,
318
},
319
320
{
321
"rosetta",
322
func(_ *flag.Flag) ([]string, error) {
323
b, err := flags.GetBool("rosetta")
324
if err != nil {
325
return nil, err
326
}
327
return []string{fmt.Sprintf(".vmOpts.vz.rosetta.enabled = %v | .vmOpts.vz.rosetta.binfmt = %v", b, b)}, nil
328
},
329
false,
330
false,
331
},
332
{"set", func(v *flag.Flag) ([]string, error) {
333
return v.Value.(flag.SliceValue).GetSlice(), nil
334
}, false, false},
335
{
336
"video",
337
func(_ *flag.Flag) ([]string, error) {
338
b, err := flags.GetBool("video")
339
if err != nil {
340
return nil, err
341
}
342
if b {
343
return []string{".video.display = \"default\""}, nil
344
}
345
return []string{".video.display = \"none\""}, nil
346
},
347
false,
348
false,
349
},
350
{"ssh-port", d(".ssh.localPort = %s"), false, false},
351
{"arch", d(".arch = %q"), true, false},
352
{
353
"containerd",
354
func(_ *flag.Flag) ([]string, error) {
355
s, err := flags.GetString("containerd")
356
if err != nil {
357
return nil, err
358
}
359
switch s {
360
case "user":
361
return []string{`.containerd.user = true | .containerd.system = false`}, nil
362
case "system":
363
return []string{`.containerd.user = false | .containerd.system = true`}, nil
364
case "user+system", "system+user":
365
return []string{`.containerd.user = true | .containerd.system = true`}, nil
366
case "none":
367
return []string{`.containerd.user = false | .containerd.system = false`}, nil
368
default:
369
return nil, fmt.Errorf(`expected one of ["user", "system", "user+system", "none"], got %q`, s)
370
}
371
},
372
true,
373
false,
374
},
375
{"disk", d(".disk= \"%sGiB\""), false, false},
376
{"plain", d(".plain = %s"), true, false},
377
{
378
"port-forward",
379
func(_ *flag.Flag) ([]string, error) {
380
ss, err := flags.GetStringArray("port-forward")
381
if err != nil {
382
return nil, err
383
}
384
value, err := BuildPortForwardExpression(ss)
385
if err != nil {
386
return nil, err
387
}
388
return []string{value}, nil
389
},
390
false,
391
false,
392
},
393
}
394
var exprs []string
395
for _, def := range defs {
396
v := flags.Lookup(def.flagName)
397
if v != nil && v.Changed {
398
if def.experimental {
399
logrus.Warnf("`--%s` is experimental", def.flagName)
400
}
401
if def.onlyValidForNewInstances && !newInstance {
402
logrus.Warnf("`--%s` is not applicable to an existing instance (Hint: create a new instance with `limactl create --%s=%s --name=NAME`)",
403
def.flagName, def.flagName, v.Value.String())
404
continue
405
}
406
newExprs, err := def.exprFunc(v)
407
if err != nil {
408
return exprs, fmt.Errorf("error while processing flag %q: %w", def.flagName, err)
409
}
410
exprs = append(exprs, newExprs...)
411
}
412
}
413
return exprs, nil
414
}
415
416
func isPowerOfTwo(x int) bool {
417
return bits.OnesCount(uint(x)) == 1
418
}
419
420
func completeCPUs(hostCPUs int) []int {
421
var res []int
422
for i := 1; i <= hostCPUs; i *= 2 {
423
res = append(res, i)
424
}
425
if !isPowerOfTwo(hostCPUs) {
426
res = append(res, hostCPUs)
427
}
428
return res
429
}
430
431
func completeMemoryGiB(hostMemory uint64) []float32 {
432
hostMemoryHalfGiB := int(hostMemory / 2 / 1024 / 1024 / 1024)
433
var res []float32
434
if hostMemoryHalfGiB < 1 {
435
res = append(res, 0.5)
436
}
437
for i := 1; i <= hostMemoryHalfGiB; i *= 2 {
438
res = append(res, float32(i))
439
}
440
if hostMemoryHalfGiB > 1 && !isPowerOfTwo(hostMemoryHalfGiB) {
441
res = append(res, float32(hostMemoryHalfGiB))
442
}
443
return res
444
}
445
446