Skip to content

Commit 72c79f6

Browse files
committed
Adds disk command and allows multiple additional disks
Signed-off-by: Sam Berning <[email protected]>
1 parent 8cc5693 commit 72c79f6

27 files changed

+665
-16
lines changed

.github/workflows/test.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ jobs:
131131
retry_on: error
132132
max_attempts: 3
133133
command: ./hack/test-example.sh examples/experimental/9p.yaml
134+
- name: "Test disk.yaml"
135+
uses: nick-invision/retry@v2
136+
with:
137+
timeout_minutes: 30
138+
retry_on: error
139+
max_attempts: 3
140+
command: ./hack/test-example.sh examples/disk.yaml
134141
# GHA macOS is slow and flaky, so we only test a few YAMLS here.
135142
# Other yamls are tested on Linux instances of Cirrus.
136143

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,14 @@ Use `<INSTANCE>:<FILENAME>` to specify a source or target inside an instance.
224224
#### `limactl edit`
225225
`limactl edit <INSTANCE>`: edit the instance
226226

227+
#### `limactl disk`
228+
229+
`limactl disk create <DISK>`: create a new external disk to attach to an instance
230+
231+
`limactl disk delete <DISK>`: delete an existing disk
232+
233+
`limactl disk list`: list all existing disks
234+
227235
#### `limactl completion`
228236
- To enable bash completion, add `source <(limactl completion bash)` to `~/.bash_profile`.
229237

cmd/limactl/disk.go

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"io/fs"
8+
"os"
9+
"text/tabwriter"
10+
11+
"github.com/docker/go-units"
12+
"github.com/lima-vm/lima/pkg/qemu"
13+
"github.com/lima-vm/lima/pkg/store"
14+
"github.com/sirupsen/logrus"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
func newDiskCommand() *cobra.Command {
19+
var diskCommand = &cobra.Command{
20+
Use: "disk",
21+
Short: "Lima disk management",
22+
Example: ` Create a disk:
23+
$ limactl disk create DISK
24+
25+
List existing disks:
26+
$ limactl disk ls
27+
28+
Delete a disk:
29+
$ limactl disk delete DISK`,
30+
SilenceUsage: true,
31+
SilenceErrors: true,
32+
}
33+
diskCommand.AddCommand(
34+
newDiskCreateCommand(),
35+
newDiskListCommand(),
36+
newDiskDeleteCommand(),
37+
newDiskUnlockCommand(),
38+
)
39+
return diskCommand
40+
}
41+
42+
func newDiskCreateCommand() *cobra.Command {
43+
var diskCreateCommand = &cobra.Command{
44+
Use: "create DISK",
45+
Example: `
46+
To create a new disk:
47+
$ limactl disk create DISK
48+
`,
49+
Short: "Create a Lima disk",
50+
Args: cobra.ExactArgs(1),
51+
RunE: diskCreateAction,
52+
}
53+
diskCreateCommand.Flags().String("size", "50G", "configure the disk size")
54+
return diskCreateCommand
55+
}
56+
57+
func diskCreateAction(cmd *cobra.Command, args []string) error {
58+
// only exactly one arg is allowed
59+
name := args[0]
60+
61+
diskDir, err := store.DiskDir(name)
62+
if err != nil {
63+
return err
64+
}
65+
66+
if _, err := os.Stat(diskDir); !errors.Is(err, fs.ErrNotExist) {
67+
return fmt.Errorf("disk %q already exists (%q)", name, diskDir)
68+
}
69+
70+
logrus.Infof("Creating a disk %q", name)
71+
72+
if err := os.MkdirAll(diskDir, 0700); err != nil {
73+
return err
74+
}
75+
76+
size, err := cmd.Flags().GetString("size")
77+
if err != nil {
78+
return err
79+
}
80+
diskSize, err := units.RAMInBytes(size)
81+
if err != nil {
82+
return err
83+
}
84+
85+
if err := qemu.CreateDataDisk(diskDir, int(diskSize)); err != nil {
86+
return err
87+
}
88+
89+
return nil
90+
}
91+
92+
func newDiskListCommand() *cobra.Command {
93+
var diskListCommand = &cobra.Command{
94+
Use: "list",
95+
Example: `
96+
To list existing disks:
97+
$ limactl disk list
98+
`,
99+
Short: "List existing Lima disks",
100+
Aliases: []string{"ls"},
101+
Args: cobra.NoArgs,
102+
RunE: diskListAction,
103+
}
104+
diskListCommand.Flags().Bool("json", false, "JSONify output")
105+
return diskListCommand
106+
}
107+
108+
func diskListAction(cmd *cobra.Command, args []string) error {
109+
jsonFormat, err := cmd.Flags().GetBool("json")
110+
if err != nil {
111+
return err
112+
}
113+
114+
allDisks, err := store.Disks()
115+
if err != nil {
116+
return err
117+
}
118+
119+
if jsonFormat {
120+
for _, diskName := range allDisks {
121+
disk, err := store.InspectDisk(diskName)
122+
if err != nil {
123+
logrus.WithError(err).Errorf("disk %q does not exist?", diskName)
124+
continue
125+
}
126+
j, err := json.Marshal(disk)
127+
if err != nil {
128+
return err
129+
}
130+
fmt.Fprintln(cmd.OutOrStdout(), string(j))
131+
}
132+
return nil
133+
}
134+
135+
w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0)
136+
fmt.Fprintln(w, "NAME\tSIZE\tDIR\tIN USE BY")
137+
138+
if len(allDisks) == 0 {
139+
logrus.Warn("No disk found. Run `limactl disk create DISK` to create a disk.")
140+
}
141+
142+
for _, diskName := range allDisks {
143+
disk, err := store.InspectDisk(diskName)
144+
if err != nil {
145+
logrus.WithError(err).Errorf("disk %q does not exist?", diskName)
146+
continue
147+
}
148+
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", disk.Name, units.BytesSize(float64(disk.Size)), disk.Dir, disk.Instance)
149+
}
150+
151+
return w.Flush()
152+
}
153+
154+
func newDiskDeleteCommand() *cobra.Command {
155+
var diskDeleteCommand = &cobra.Command{
156+
Use: "delete DISK [DISK, ...]",
157+
Example: `
158+
To delete a disk:
159+
$ limactl disk delete DISK
160+
161+
To delete multiple disks:
162+
$ limactl disk delete DISK1 DISK2 ...
163+
`,
164+
Aliases: []string{"remove", "rm"},
165+
Short: "Delete one or more Lima disks",
166+
Args: cobra.MinimumNArgs(1),
167+
RunE: diskDeleteAction,
168+
}
169+
diskDeleteCommand.Flags().Bool("force", false, "force delete")
170+
return diskDeleteCommand
171+
}
172+
173+
func diskDeleteAction(cmd *cobra.Command, args []string) error {
174+
force, err := cmd.Flags().GetBool("force")
175+
if err != nil {
176+
return err
177+
}
178+
179+
for _, diskName := range args {
180+
if force {
181+
disk, err := store.InspectDisk(diskName)
182+
if err != nil {
183+
if errors.Is(err, fs.ErrNotExist) {
184+
logrus.Warnf("Ignoring non-existent disk %q", diskName)
185+
continue
186+
}
187+
return err
188+
}
189+
190+
if err := deleteDisk(disk); err != nil {
191+
return fmt.Errorf("failed to delete disk %q: %w", diskName, err)
192+
}
193+
logrus.Infof("Deleted %q (%q)", diskName, disk.Dir)
194+
continue
195+
}
196+
197+
disk, err := store.InspectDisk(diskName)
198+
if err != nil {
199+
if errors.Is(err, fs.ErrNotExist) {
200+
logrus.Warnf("Ignoring non-existent disk %q", diskName)
201+
continue
202+
}
203+
return err
204+
}
205+
if disk.Instance != "" {
206+
return fmt.Errorf("cannot delete disk %q in use by instance %q", disk.Name, disk.Instance)
207+
}
208+
instances, err := store.Instances()
209+
if err != nil {
210+
return err
211+
}
212+
var refInstances []string
213+
for _, instName := range instances {
214+
inst, err := store.Inspect(instName)
215+
if err != nil {
216+
continue
217+
}
218+
if len(inst.AdditionalDisks) > 0 {
219+
for _, d := range inst.AdditionalDisks {
220+
if d == diskName {
221+
refInstances = append(refInstances, instName)
222+
}
223+
}
224+
}
225+
}
226+
if len(refInstances) > 0 {
227+
logrus.Warnf("Skipping deleting disk %q, disk is referenced by one or more non-running instances: %q",
228+
diskName, refInstances)
229+
logrus.Warnf("To delete anyway, run %q", forceDeleteCommand(diskName))
230+
continue
231+
}
232+
if err := deleteDisk(disk); err != nil {
233+
return fmt.Errorf("failed to delete disk %q: %v", diskName, err)
234+
}
235+
logrus.Infof("Deleted %q (%q)", diskName, disk.Dir)
236+
}
237+
return nil
238+
}
239+
240+
func deleteDisk(disk *store.Disk) error {
241+
if err := os.RemoveAll(disk.Dir); err != nil {
242+
return fmt.Errorf("failed to remove %q: %w", disk.Dir, err)
243+
}
244+
return nil
245+
}
246+
247+
func forceDeleteCommand(diskName string) string {
248+
return fmt.Sprintf("limactl disk delete --force %v", diskName)
249+
}
250+
251+
func newDiskUnlockCommand() *cobra.Command {
252+
var diskUnlockCommand = &cobra.Command{
253+
Use: "unlock DISK [DISK, ...]",
254+
Example: `
255+
Emergency recovery! If an instance is force stopped, it may leave a disk locked while not actually using it.
256+
257+
To unlock a disk:
258+
$ limactl disk unlock DISK
259+
260+
To unlock multiple disks:
261+
$ limactl disk unlock DISK1 DISK2 ...
262+
`,
263+
Short: "Unlock one or more Lima disks",
264+
Args: cobra.MinimumNArgs(1),
265+
RunE: diskUnlockAction,
266+
}
267+
return diskUnlockCommand
268+
}
269+
270+
func diskUnlockAction(cmd *cobra.Command, args []string) error {
271+
for _, diskName := range args {
272+
disk, err := store.InspectDisk(diskName)
273+
if err != nil {
274+
if errors.Is(err, fs.ErrNotExist) {
275+
logrus.Warnf("Ignoring non-existent disk %q", diskName)
276+
continue
277+
}
278+
return err
279+
}
280+
if disk.Instance == "" {
281+
logrus.Warnf("Ignoring unlocked disk %q", diskName)
282+
continue
283+
}
284+
// if store.Inspect throws an error, the instance does not exist, and it is safe to unlock
285+
inst, err := store.Inspect(disk.Instance)
286+
if err == nil {
287+
if len(inst.Errors) > 0 {
288+
logrus.Warnf("Cannot unlock disk %q, attached instance %q has errors: %+v",
289+
diskName, disk.Instance, inst.Errors)
290+
continue
291+
}
292+
if inst.Status == store.StatusRunning {
293+
logrus.Warnf("Cannot unlock disk %q used by running instance %q", diskName, disk.Instance)
294+
continue
295+
}
296+
}
297+
if err := disk.Unlock(); err != nil {
298+
return fmt.Errorf("failed to unlock disk %q: %w", diskName, err)
299+
}
300+
logrus.Infof("Unlocked disk %q (%q)", diskName, disk.Dir)
301+
}
302+
return nil
303+
}

cmd/limactl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ func newApp() *cobra.Command {
9292
newDebugCommand(),
9393
newEditCommand(),
9494
newFactoryResetCommand(),
95+
newDiskCommand(),
9596
)
9697
return rootCmd
9798
}

cmd/limactl/stop.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,17 @@ func stopInstanceForcibly(inst *store.Instance) {
113113
logrus.Info("The QEMU process seems already stopped")
114114
}
115115

116+
for _, diskName := range inst.AdditionalDisks {
117+
disk, err := store.InspectDisk(diskName)
118+
if err != nil {
119+
logrus.Warnf("Disk %q does not exist", diskName)
120+
continue
121+
}
122+
if err := disk.Unlock(); err != nil {
123+
logrus.Warnf("Failed to unlock disk %q. To use, run `limactl disk unlock %v`", diskName, diskName)
124+
}
125+
}
126+
116127
if inst.HostAgentPID > 0 {
117128
logrus.Infof("Sending SIGKILL to the host agent process %d", inst.HostAgentPID)
118129
if err := osutil.SysKill(inst.HostAgentPID, osutil.SigKill); err != nil {

docs/internal.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ Host agent:
5959
- `ha.stdout.log`: hostagent stdout (JSON lines, see `pkg/hostagent/events.Event`)
6060
- `ha.stderr.log`: hostagent stderr (human-readable messages)
6161

62+
## Disk directory (`${LIMA_HOME}/_disk/<DISK>`)
63+
64+
A disk directory contains the following files:
65+
66+
data disk:
67+
- `datadisk`: the qcow2 disk that is attached to an instance
68+
69+
lock:
70+
- `in_use_by`: symlink to the instance directory that is using the disk
71+
6272
## Lima cache directory (`~/Library/Caches/lima`)
6373

6474
Currently hard-coded to `~/Library/Caches/lima` on macOS.

examples/default.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ mounts:
9292
# 🟢 Builtin default: "reverse-sshfs"
9393
mountType: null
9494

95+
# Lima disks to attach to the instance. The disks will be accessible from inside the
96+
# instance, labeled by name. (e.g. if the disk is named "data", it will be labeled
97+
# "lima-data" inside the instance). The disk will be mounted inside the instance at
98+
# `/mnt/lima-${VOLUME}`.
99+
# 🟢 Builtin default: null
100+
additionalDisks:
101+
# disks should be a list of disk name strings, for example:
102+
# - "data"
103+
95104
ssh:
96105
# A localhost port of the host. Forwarded to port 22 of the guest.
97106
# 🟢 Builtin default: 0 (automatically assigned to a free port)

0 commit comments

Comments
 (0)