Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Storage: Attach VM snapshots as disk devices #14930

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
4 changes: 2 additions & 2 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2549,8 +2549,8 @@ This adds support for listing network zones across all projects using the `all-p

## `instance_root_volume_attachment`
Copy link
Member

@tomponline tomponline Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MggMuggins shall we rename this to vm_root_volume_attachment as my understanding is that we dont support containers yet, and we dont plan to before LXD 6.3 is released?


Adds support for instance root volumes to be attached to other instances as disk
devices. Introduces the `<type>/<volume>` syntax for the `source` property of
Adds support for instance root volumes and snapshots to be attached to other
instances as disk devices. Introduces the `source.type` and `source.snapshot` keys for
disk devices.

## `projects_limits_uplink_ips`
Expand Down
7 changes: 7 additions & 0 deletions doc/howto/storage_volumes.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,13 @@ can be unset from `vm1`:
`security.shared` can also be used on `virtual-machine` volumes to enable concurrent
access. Note that concurrent access to block volumes may result in data loss.

### Attaching virtual machine snapshots to other instances
Virtual-machine snapshots can also be attached to instances with the
{config:option}`device-disk-device-conf:source.snapshot` disk device
configuration key.

lxc config device add v1 v2-root-snap0 disk pool=my-pool source=vm2 source.type=virtual-machine source.snapshot=snap0

## Resize a storage volume

If you need more storage in a volume, you can increase the size of your storage volume.
Expand Down
7 changes: 7 additions & 0 deletions doc/metadata.txt
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@ See {ref}`devices-disk-types` for details.

```

```{config:option} source.snapshot device-disk-device-conf
:required: "no"
:shortdesc: "`source` snapshot name"
:type: "string"
Snapshot of the volume given by `source`.
```

```{config:option} source.type device-disk-device-conf
:defaultdesc: "`custom`"
:required: "no"
Expand Down
2 changes: 1 addition & 1 deletion doc/reference/devices_disk.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ See {ref}`devices-disk-examples` for more detailed information on how to add eac

Storage volume
: The most common type of disk device is a storage volume.
Specify the storage volume name as the source to add a storage volume as a disk device.
Specify the storage volume name as the {config:option}`device-disk-device-conf:source` to add a storage volume as a disk device. `virtual-machine' storage volumes (and their snapshots) can also be attached as disk devices.

Path on the host
: You can share a path on your host (either a file system or a block device) to your instance.
Expand Down
187 changes: 85 additions & 102 deletions lxc/storage_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ func parseVolume(defaultType string, name string) (volName string, volType strin
return volName, volType
}

func parseVolumeSnapshot(defaultType string, name string) (volName string, volType string, snapshot string) {
volName, volType = parseVolume(defaultType, name)

parts := strings.SplitN(volName, "/", 2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strings.Cut would also be an option here

if len(parts) == 2 {
volName, snapshot = parts[0], parts[1]
}

return volName, volType, snapshot
}

func (c *cmdStorageVolume) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("volume")
Expand Down Expand Up @@ -155,6 +166,68 @@ func (c *cmdStorageVolume) parseVolumeWithPool(name string) (volumeName string,
return fields[1], fields[0]
}

func cmdAttachArgsAsDevice(client lxd.InstanceServer, poolName string, flagTarget string, args []string) (devName string, device map[string]string, err error) {
volName, volType, snapshot := parseVolumeSnapshot("custom", args[1])
if volType != "custom" && volType != "virtual-machine" {
return "", nil, errors.New(i18n.G(`Only "custom" and "virtual-machine" volumes can be attached to instances`))
}

// Attach the volume
devPath := ""
if len(args) == 3 {
devName = args[1]
} else if len(args) == 4 {
// Use the provided target.
if flagTarget != "" && client.IsClustered() {
client = client.UseTarget(flagTarget)
}

vol, _, err := client.GetStoragePoolVolume(poolName, volType, volName)
if err != nil {
return "", nil, err
}

switch vol.ContentType {
case "block", "iso":
devName = args[3]
case "filesystem":
// If using a filesystem volume, the path must also be provided as the fourth argument.
if !strings.HasPrefix(args[3], "/") {
devPath = path.Join("/", args[3])
} else {
devPath = args[3]
}

devName = args[1]
default:
return "", nil, errors.New(i18n.G("Unsupported content type for attaching to instances"))
}
} else if len(args) == 5 {
// Path and device name have been given to us.
devName = args[3]
devPath = args[4]
}

// Prepare the instance's device entry
device = map[string]string{
"type": "disk",
"pool": poolName,
"source": volName,
"path": devPath,
}

// Only specify sourcetype when not the default
if volType != "custom" {
device["source.type"] = volType
}

if snapshot != "" {
device["source.snapshot"] = snapshot
}

return devName, device, nil
}

// Attach.
type cmdStorageVolumeAttach struct {
global *cmdGlobal
Expand All @@ -164,7 +237,7 @@ type cmdStorageVolumeAttach struct {

func (c *cmdStorageVolumeAttach) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("attach", i18n.G("[<remote>:]<pool> [<type>/]<volume> <instance> [<device name>] [<path>]"))
cmd.Use = usage("attach", i18n.G("[<remote>:]<pool> [<type>/]<volume>[/<snapshot>] <instance> [<device name>] [<path>]"))
cmd.Short = i18n.G("Attach new storage volumes to instances")
cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
`Attach new storage volumes to instances
Expand Down Expand Up @@ -211,61 +284,9 @@ func (c *cmdStorageVolumeAttach) run(cmd *cobra.Command, args []string) error {
return errors.New(i18n.G("Missing pool name"))
}

volName, volType := parseVolume("custom", args[1])
if volType != "custom" && volType != "virtual-machine" {
return errors.New(i18n.G(`Only "custom" and "virtual-machine" volumes can be attached to instances`))
}

// Attach the volume
devPath := ""
devName := ""
if len(args) == 3 {
devName = args[1]
} else if len(args) == 4 {
client := resource.server

// Use the provided target.
if c.storage.flagTarget != "" && client.IsClustered() {
client = client.UseTarget(c.storage.flagTarget)
}

vol, _, err := client.GetStoragePoolVolume(resource.name, volType, volName)
if err != nil {
return err
}

switch vol.ContentType {
case "block", "iso":
devName = args[3]
case "filesystem":
// If using a filesystem volume, the path must also be provided as the fourth argument.
if !strings.HasPrefix(args[3], "/") {
devPath = path.Join("/", args[3])
} else {
devPath = args[3]
}

devName = args[1]
default:
return errors.New(i18n.G("Unsupported content type for attaching to instances"))
}
} else if len(args) == 5 {
// Path and device name have been given to us.
devName = args[3]
devPath = args[4]
}

// Prepare the instance's device entry
device := map[string]string{
"type": "disk",
"pool": resource.name,
"source": volName,
"path": devPath,
}

// Only specify sourcetype when not the default
if volType != "custom" {
device["source.type"] = volType
devName, device, err := cmdAttachArgsAsDevice(resource.server, resource.name, c.storage.flagTarget, args)
if err != nil {
return err
}

// Add the device to the instance
Expand All @@ -286,7 +307,7 @@ type cmdStorageVolumeAttachProfile struct {

func (c *cmdStorageVolumeAttachProfile) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("attach-profile", i18n.G("[<remote:>]<pool> [<type>/]<volume> <profile> [<device name>] [<path>]"))
cmd.Use = usage("attach-profile", i18n.G("[<remote:>]<pool> [<type>/]<volume>[/<snapshot>] <profile> [<device name>] [<path>]"))
cmd.Short = i18n.G("Attach new storage volumes to profiles")
cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
`Attach new storage volumes to profiles
Expand Down Expand Up @@ -333,49 +354,11 @@ func (c *cmdStorageVolumeAttachProfile) run(cmd *cobra.Command, args []string) e
return errors.New(i18n.G("Missing pool name"))
}

// Attach the volume
devPath := ""
devName := ""
if len(args) == 3 {
devName = args[1]
} else if len(args) == 4 {
// Only the path has been given to us.
devPath = args[3]
devName = args[1]
} else if len(args) == 5 {
// Path and device name have been given to us.
devName = args[3]
devPath = args[4]
}

volName, volType := parseVolume("custom", args[1])
if volType != "custom" && volType != "virtual-machine" {
return errors.New(i18n.G(`Only "custom" and "virtual-machine" volumes can be attached to profiles`))
}

// Check if the requested storage volume actually exists
vol, _, err := resource.server.GetStoragePoolVolume(resource.name, volType, volName)
devName, device, err := cmdAttachArgsAsDevice(resource.server, resource.name, c.storage.flagTarget, args)
if err != nil {
return err
}

// Prepare the instance's device entry
device := map[string]string{
"type": "disk",
"pool": resource.name,
"source": volName,
}

// Ignore path for block volumes
if vol.ContentType != "block" {
device["path"] = devPath
}

// Only specify sourcetype when not the default
if volType != "custom" {
device["source.type"] = volType
}

// Add the device to the instance
err = profileDeviceAdd(resource.server, args[2], devName, device)
if err != nil {
Expand Down Expand Up @@ -817,7 +800,7 @@ type cmdStorageVolumeDetach struct {

func (c *cmdStorageVolumeDetach) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("detach", i18n.G("[<remote>:]<pool> [<type>/]<volume> <instance> [<device name>]"))
cmd.Use = usage("detach", i18n.G("[<remote>:]<pool> [<type>/]<volume>[/<snapshot>] <instance> [<device name>]"))
cmd.Short = i18n.G("Detach storage volumes from instances")
cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
`Detach storage volumes from instances`))
Expand Down Expand Up @@ -874,7 +857,7 @@ func (c *cmdStorageVolumeDetach) run(cmd *cobra.Command, args []string) error {
return err
}

volName, volType := parseVolume("custom", args[1])
volName, volType, snapshot := parseVolumeSnapshot("custom", args[1])

// Find the device
if devName == "" {
Expand All @@ -884,7 +867,7 @@ func (c *cmdStorageVolumeDetach) run(cmd *cobra.Command, args []string) error {
sourceType = d["source.type"]
}

if d["type"] == "disk" && d["pool"] == resource.name && volType == sourceType && volName == d["source"] {
if d["type"] == "disk" && d["pool"] == resource.name && volType == sourceType && volName == d["source"] && snapshot == d["source.snapshot"] {
if devName != "" {
return errors.New(i18n.G("More than one device matches, specify the device name"))
}
Expand Down Expand Up @@ -922,7 +905,7 @@ type cmdStorageVolumeDetachProfile struct {

func (c *cmdStorageVolumeDetachProfile) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("detach-profile", i18n.G("[<remote:>]<pool> [<type>/]<volume> <profile> [<device name>]"))
cmd.Use = usage("detach-profile", i18n.G("[<remote:>]<pool> [<type>/]<volume>[/<snapshot>] <profile> [<device name>]"))
cmd.Short = i18n.G("Detach storage volumes from profiles")
cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
`Detach storage volumes from profiles`))
Expand Down Expand Up @@ -978,7 +961,7 @@ func (c *cmdStorageVolumeDetachProfile) run(cmd *cobra.Command, args []string) e
return err
}

volName, volType := parseVolume("custom", args[1])
volName, volType, snapshot := parseVolumeSnapshot("custom", args[1])

// Find the device
if devName == "" {
Expand All @@ -988,7 +971,7 @@ func (c *cmdStorageVolumeDetachProfile) run(cmd *cobra.Command, args []string) e
sourceType = d["source.type"]
}

if d["type"] == "disk" && d["pool"] == resource.name && volType == sourceType && volName == d["source"] {
if d["type"] == "disk" && d["pool"] == resource.name && volType == sourceType && volName == d["source"] && snapshot == d["source.snapshot"] {
if devName != "" {
return errors.New(i18n.G("More than one device matches, specify the device name"))
}
Expand Down
Loading
Loading