Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/cmd/limactl/disk.go
1645 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package main
5
6
import (
7
"encoding/json"
8
"errors"
9
"fmt"
10
"io/fs"
11
"os"
12
"path/filepath"
13
"text/tabwriter"
14
15
contfs "github.com/containerd/continuity/fs"
16
"github.com/docker/go-units"
17
"github.com/lima-vm/go-qcow2reader"
18
"github.com/sirupsen/logrus"
19
"github.com/spf13/cobra"
20
21
"github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil"
22
"github.com/lima-vm/lima/v2/pkg/limatype"
23
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
24
"github.com/lima-vm/lima/v2/pkg/store"
25
)
26
27
func newDiskCommand() *cobra.Command {
28
diskCommand := &cobra.Command{
29
Use: "disk",
30
Short: "Lima disk management",
31
Example: ` Create a disk:
32
$ limactl disk create DISK --size SIZE [--format qcow2]
33
34
List existing disks:
35
$ limactl disk ls
36
37
Delete a disk:
38
$ limactl disk delete DISK
39
40
Resize a disk:
41
$ limactl disk resize DISK --size SIZE`,
42
SilenceUsage: true,
43
SilenceErrors: true,
44
GroupID: advancedCommand,
45
}
46
diskCommand.AddCommand(
47
newDiskCreateCommand(),
48
newDiskListCommand(),
49
newDiskDeleteCommand(),
50
newDiskUnlockCommand(),
51
newDiskResizeCommand(),
52
newDiskImportCommand(),
53
)
54
return diskCommand
55
}
56
57
func newDiskCreateCommand() *cobra.Command {
58
diskCreateCommand := &cobra.Command{
59
Use: "create DISK",
60
Example: `
61
To create a new disk:
62
$ limactl disk create DISK --size SIZE [--format qcow2]
63
`,
64
Short: "Create a Lima disk",
65
Args: WrapArgsError(cobra.ExactArgs(1)),
66
RunE: diskCreateAction,
67
}
68
diskCreateCommand.Flags().String("size", "", "Configure the disk size")
69
_ = diskCreateCommand.MarkFlagRequired("size")
70
diskCreateCommand.Flags().String("format", "qcow2", "Specify the disk format")
71
return diskCreateCommand
72
}
73
74
func diskCreateAction(cmd *cobra.Command, args []string) error {
75
ctx := cmd.Context()
76
size, err := cmd.Flags().GetString("size")
77
if err != nil {
78
return err
79
}
80
81
format, err := cmd.Flags().GetString("format")
82
if err != nil {
83
return err
84
}
85
86
diskSize, err := units.RAMInBytes(size)
87
if err != nil {
88
return err
89
}
90
91
switch format {
92
case "qcow2", "raw":
93
default:
94
return fmt.Errorf(`disk format %q not supported, use "qcow2" or "raw" instead`, format)
95
}
96
97
// only exactly one arg is allowed
98
name := args[0]
99
100
diskDir, err := store.DiskDir(name)
101
if err != nil {
102
return err
103
}
104
105
if _, err := os.Stat(diskDir); !errors.Is(err, fs.ErrNotExist) {
106
return fmt.Errorf("disk %q already exists (%q)", name, diskDir)
107
}
108
109
logrus.Infof("Creating %s disk %q with size %s", format, name, units.BytesSize(float64(diskSize)))
110
111
if err := os.MkdirAll(diskDir, 0o700); err != nil {
112
return err
113
}
114
115
// qemu may not be available, use it only if needed.
116
dataDisk := filepath.Join(diskDir, filenames.DataDisk)
117
diskUtil := proxyimgutil.NewDiskUtil(ctx)
118
err = diskUtil.CreateDisk(ctx, dataDisk, diskSize)
119
if err != nil {
120
rerr := os.RemoveAll(diskDir)
121
if rerr != nil {
122
err = errors.Join(err, fmt.Errorf("failed to remove a directory %q: %w", diskDir, rerr))
123
}
124
return fmt.Errorf("failed to create %s disk in %q: %w", format, diskDir, err)
125
}
126
127
return nil
128
}
129
130
func newDiskListCommand() *cobra.Command {
131
diskListCommand := &cobra.Command{
132
Use: "list",
133
Example: `
134
To list existing disks:
135
$ limactl disk list
136
`,
137
Short: "List existing Lima disks",
138
Aliases: []string{"ls"},
139
Args: WrapArgsError(cobra.ArbitraryArgs),
140
RunE: diskListAction,
141
}
142
diskListCommand.Flags().Bool("json", false, "JSONify output")
143
return diskListCommand
144
}
145
146
func nameMatches(nameName string, names []string) []string {
147
matches := []string{}
148
for _, name := range names {
149
if name == nameName {
150
matches = append(matches, name)
151
}
152
}
153
return matches
154
}
155
156
func diskListAction(cmd *cobra.Command, args []string) error {
157
jsonFormat, err := cmd.Flags().GetBool("json")
158
if err != nil {
159
return err
160
}
161
162
allDisks, err := store.Disks()
163
if err != nil {
164
return err
165
}
166
167
disks := []string{}
168
if len(args) > 0 {
169
for _, arg := range args {
170
matches := nameMatches(arg, allDisks)
171
if len(matches) > 0 {
172
disks = append(disks, matches...)
173
} else {
174
logrus.Warnf("No disk matching %v found.", arg)
175
}
176
}
177
} else {
178
disks = allDisks
179
}
180
181
if jsonFormat {
182
for _, diskName := range disks {
183
disk, err := store.InspectDisk(diskName)
184
if err != nil {
185
logrus.WithError(err).Errorf("disk %q does not exist?", diskName)
186
continue
187
}
188
j, err := json.Marshal(disk)
189
if err != nil {
190
return err
191
}
192
fmt.Fprintln(cmd.OutOrStdout(), string(j))
193
}
194
return nil
195
}
196
197
w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0)
198
fmt.Fprintln(w, "NAME\tSIZE\tFORMAT\tDIR\tIN-USE-BY")
199
200
if len(disks) == 0 {
201
logrus.Warn("No disk found. Run `limactl disk create DISK --size SIZE` to create a disk.")
202
}
203
204
for _, diskName := range disks {
205
disk, err := store.InspectDisk(diskName)
206
if err != nil {
207
logrus.WithError(err).Errorf("disk %q does not exist?", diskName)
208
continue
209
}
210
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", disk.Name, units.BytesSize(float64(disk.Size)), disk.Format, disk.Dir, disk.Instance)
211
}
212
213
return w.Flush()
214
}
215
216
func newDiskDeleteCommand() *cobra.Command {
217
diskDeleteCommand := &cobra.Command{
218
Use: "delete DISK [DISK, ...]",
219
Example: `
220
To delete a disk:
221
$ limactl disk delete DISK
222
223
To delete multiple disks:
224
$ limactl disk delete DISK1 DISK2 ...
225
`,
226
Aliases: []string{"remove", "rm"},
227
Short: "Delete one or more Lima disks",
228
Args: WrapArgsError(cobra.MinimumNArgs(1)),
229
RunE: diskDeleteAction,
230
ValidArgsFunction: diskBashComplete,
231
}
232
diskDeleteCommand.Flags().BoolP("force", "f", false, "Force delete")
233
return diskDeleteCommand
234
}
235
236
func diskDeleteAction(cmd *cobra.Command, args []string) error {
237
ctx := cmd.Context()
238
force, err := cmd.Flags().GetBool("force")
239
if err != nil {
240
return err
241
}
242
243
instNames, err := store.Instances()
244
if err != nil {
245
return err
246
}
247
var instances []*limatype.Instance
248
for _, instName := range instNames {
249
inst, err := store.Inspect(ctx, instName)
250
if err != nil {
251
continue
252
}
253
instances = append(instances, inst)
254
}
255
256
for _, diskName := range args {
257
disk, err := store.InspectDisk(diskName)
258
if err != nil {
259
if errors.Is(err, fs.ErrNotExist) {
260
logrus.Warnf("Ignoring non-existent disk %q", diskName)
261
continue
262
}
263
return err
264
}
265
266
if !force {
267
if disk.Instance != "" {
268
return fmt.Errorf("cannot delete disk %q in use by instance %q", disk.Name, disk.Instance)
269
}
270
var refInstances []string
271
for _, inst := range instances {
272
for _, d := range inst.AdditionalDisks {
273
if d.Name == diskName {
274
refInstances = append(refInstances, inst.Name)
275
}
276
}
277
}
278
if len(refInstances) > 0 {
279
logrus.Warnf("Skipping deleting disk %q, disk is referenced by one or more non-running instances: %q",
280
diskName, refInstances)
281
logrus.Warnf("To delete anyway, run %q", forceDeleteCommand(diskName))
282
continue
283
}
284
}
285
286
if err := deleteDisk(disk); err != nil {
287
return fmt.Errorf("failed to delete disk %q: %w", diskName, err)
288
}
289
logrus.Infof("Deleted %q (%q)", diskName, disk.Dir)
290
}
291
return nil
292
}
293
294
func deleteDisk(disk *store.Disk) error {
295
if err := os.RemoveAll(disk.Dir); err != nil {
296
return fmt.Errorf("failed to remove %q: %w", disk.Dir, err)
297
}
298
return nil
299
}
300
301
func forceDeleteCommand(diskName string) string {
302
return fmt.Sprintf("limactl disk delete --force %v", diskName)
303
}
304
305
func newDiskUnlockCommand() *cobra.Command {
306
diskUnlockCommand := &cobra.Command{
307
Use: "unlock DISK [DISK, ...]",
308
Example: `
309
Emergency recovery! If an instance is force stopped, it may leave a disk locked while not actually using it.
310
311
To unlock a disk:
312
$ limactl disk unlock DISK
313
314
To unlock multiple disks:
315
$ limactl disk unlock DISK1 DISK2 ...
316
`,
317
Short: "Unlock one or more Lima disks",
318
Args: WrapArgsError(cobra.MinimumNArgs(1)),
319
RunE: diskUnlockAction,
320
ValidArgsFunction: diskBashComplete,
321
}
322
return diskUnlockCommand
323
}
324
325
func diskUnlockAction(cmd *cobra.Command, args []string) error {
326
ctx := cmd.Context()
327
for _, diskName := range args {
328
disk, err := store.InspectDisk(diskName)
329
if err != nil {
330
if errors.Is(err, fs.ErrNotExist) {
331
logrus.Warnf("Ignoring non-existent disk %q", diskName)
332
continue
333
}
334
return err
335
}
336
if disk.Instance == "" {
337
logrus.Warnf("Ignoring unlocked disk %q", diskName)
338
continue
339
}
340
// if store.Inspect throws an error, the instance does not exist, and it is safe to unlock
341
inst, err := store.Inspect(ctx, disk.Instance)
342
if err == nil {
343
if len(inst.Errors) > 0 {
344
logrus.Warnf("Cannot unlock disk %q, attached instance %q has errors: %+v",
345
diskName, disk.Instance, inst.Errors)
346
continue
347
}
348
if inst.Status == limatype.StatusRunning {
349
logrus.Warnf("Cannot unlock disk %q used by running instance %q", diskName, disk.Instance)
350
continue
351
}
352
}
353
if err := disk.Unlock(); err != nil {
354
return fmt.Errorf("failed to unlock disk %q: %w", diskName, err)
355
}
356
logrus.Infof("Unlocked disk %q (%q)", diskName, disk.Dir)
357
}
358
return nil
359
}
360
361
func newDiskResizeCommand() *cobra.Command {
362
diskResizeCommand := &cobra.Command{
363
Use: "resize DISK",
364
Example: `
365
Resize a disk:
366
$ limactl disk resize DISK --size SIZE`,
367
Short: "Resize existing Lima disk",
368
Args: WrapArgsError(cobra.ExactArgs(1)),
369
RunE: diskResizeAction,
370
ValidArgsFunction: diskBashComplete,
371
}
372
diskResizeCommand.Flags().String("size", "", "Disk size")
373
_ = diskResizeCommand.MarkFlagRequired("size")
374
return diskResizeCommand
375
}
376
377
func diskResizeAction(cmd *cobra.Command, args []string) error {
378
ctx := cmd.Context()
379
size, err := cmd.Flags().GetString("size")
380
if err != nil {
381
return err
382
}
383
384
diskSize, err := units.RAMInBytes(size)
385
if err != nil {
386
return err
387
}
388
389
diskName := args[0]
390
disk, err := store.InspectDisk(diskName)
391
if err != nil {
392
if errors.Is(err, fs.ErrNotExist) {
393
return fmt.Errorf("disk %q does not exists", diskName)
394
}
395
return err
396
}
397
398
// Shrinking can cause a disk failure
399
if diskSize < disk.Size {
400
return fmt.Errorf("specified size %q is less than the current disk size %q. Disk shrinking is currently unavailable", units.BytesSize(float64(diskSize)), units.BytesSize(float64(disk.Size)))
401
}
402
403
if disk.Instance != "" {
404
inst, err := store.Inspect(ctx, disk.Instance)
405
if err == nil {
406
if inst.Status == limatype.StatusRunning {
407
return fmt.Errorf("cannot resize disk %q used by running instance %q. Please stop the VM instance", diskName, disk.Instance)
408
}
409
}
410
}
411
412
// qemu may not be available, use it only if needed.
413
dataDisk := filepath.Join(disk.Dir, filenames.DataDisk)
414
diskUtil := proxyimgutil.NewDiskUtil(ctx)
415
err = diskUtil.ResizeDisk(ctx, dataDisk, diskSize)
416
if err != nil {
417
return fmt.Errorf("failed to resize disk %q: %w", diskName, err)
418
}
419
420
logrus.Infof("Resized disk %q (%q)", diskName, disk.Dir)
421
return nil
422
}
423
424
func diskBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
425
return bashCompleteDiskNames(cmd)
426
}
427
428
func newDiskImportCommand() *cobra.Command {
429
diskImportCommand := &cobra.Command{
430
Use: "import DISK FILE",
431
Example: `
432
Import a disk:
433
$ limactl disk import DISK DISKPATH
434
`,
435
Short: "Import an existing disk to Lima",
436
Args: WrapArgsError(cobra.ExactArgs(2)),
437
RunE: diskImportAction,
438
ValidArgsFunction: diskBashComplete,
439
}
440
return diskImportCommand
441
}
442
443
func diskImportAction(_ *cobra.Command, args []string) error {
444
diskName := args[0]
445
fName := args[1]
446
447
diskDir, err := store.DiskDir(diskName)
448
if err != nil {
449
return err
450
}
451
452
if _, err := os.Stat(diskDir); !errors.Is(err, fs.ErrNotExist) {
453
return fmt.Errorf("disk %q already exists (%q)", diskName, diskDir)
454
}
455
456
f, err := os.Open(fName)
457
if err != nil {
458
return err
459
}
460
defer f.Close()
461
462
img, err := qcow2reader.Open(f)
463
if err != nil {
464
return err
465
}
466
467
diskSize := img.Size()
468
format := img.Type()
469
470
switch format {
471
case "qcow2", "raw":
472
default:
473
return fmt.Errorf(`disk format %q not supported, use "qcow2" or "raw" instead`, format)
474
}
475
476
if err := os.MkdirAll(diskDir, 0o755); err != nil {
477
return err
478
}
479
480
if err := contfs.CopyFile(filepath.Join(diskDir, filenames.DataDisk), fName); err != nil {
481
return nil
482
}
483
484
logrus.Infof("Imported %s with size %s", diskName, units.BytesSize(float64(diskSize)))
485
486
return nil
487
}
488
489