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