Skip to content

Commit 06a4b33

Browse files
authored
Merge pull request #1065 from sam-berning/volume
Add `disk` command and allows multiple additional disks
2 parents 4a4cc62 + c3bb4cd commit 06a4b33

27 files changed

+668
-16
lines changed

.github/workflows/test.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ jobs:
135135
retry_on: error
136136
max_attempts: 3
137137
command: ./hack/test-example.sh examples/experimental/9p.yaml
138+
- name: "Test disk.yaml"
139+
uses: nick-invision/retry@v2
140+
with:
141+
timeout_minutes: 30
142+
retry_on: error
143+
max_attempts: 3
144+
command: ./hack/test-example.sh examples/disk.yaml
138145
# GHA macOS is slow and flaky, so we only test a few YAMLS here.
139146
# Other yamls are tested on Linux instances of Cirrus.
140147

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

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)