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
39 changes: 29 additions & 10 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 @@ -164,7 +175,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,7 +222,7 @@ func (c *cmdStorageVolumeAttach) run(cmd *cobra.Command, args []string) error {
return errors.New(i18n.G("Missing pool name"))
}

volName, volType := parseVolume("custom", args[1])
volName, volType, snapshot := parseVolumeSnapshot("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`))
}
Expand Down Expand Up @@ -268,6 +279,10 @@ func (c *cmdStorageVolumeAttach) run(cmd *cobra.Command, args []string) error {
device["source.type"] = volType
}

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

// Add the device to the instance
err = instanceDeviceAdd(resource.server, args[2], devName, device)
if err != nil {
Expand All @@ -286,7 +301,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 @@ -348,7 +363,7 @@ func (c *cmdStorageVolumeAttachProfile) run(cmd *cobra.Command, args []string) e
devPath = args[4]
}

volName, volType := parseVolume("custom", args[1])
volName, volType, snapshot := parseVolumeSnapshot("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`))
}
Expand Down Expand Up @@ -376,6 +391,10 @@ func (c *cmdStorageVolumeAttachProfile) run(cmd *cobra.Command, args []string) e
device["source.type"] = volType
}

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

// Add the device to the instance
err = profileDeviceAdd(resource.server, args[2], devName, device)
if err != nil {
Expand Down Expand Up @@ -817,7 +836,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 +893,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 +903,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 +941,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 +997,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 +1007,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
48 changes: 42 additions & 6 deletions lxd/device/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ func (d *disk) sourceIsLocalPath(source string) bool {
func (d *disk) sourceVolumeFields() (volumeName string, volumeType storageDrivers.VolumeType, dbVolumeType int, volumeTypeName string, err error) {
volumeName = d.config["source"]

if d.config["source.snapshot"] != "" {
volumeName = volumeName + shared.SnapshotDelimiter + d.config["source.snapshot"]
}

volumeTypeName = cluster.StoragePoolVolumeTypeNameCustom
if d.config["source.type"] != "" {
volumeTypeName = d.config["source.type"]
Expand Down Expand Up @@ -281,6 +285,13 @@ func (d *disk) validateConfig(instConf instance.ConfigReader) error {
// required: no
// shortdesc: Type of the backing storage volume
"source.type": validate.Optional(validate.IsOneOf(cluster.StoragePoolVolumeTypeNameCustom, cluster.StoragePoolVolumeTypeNameVM)),
// lxdmeta:generate(entities=device-disk; group=device-conf; key=source.snapshot)
// Snapshot of the volume given by `source`.
// ---
// type: string
// required: no
// shortdesc: `source` snapshot name
"source.snapshot": validate.IsAny,
// lxdmeta:generate(entities=device-disk; group=device-conf; key=limits.read)
// You can specify a value in byte/s (various suffixes supported, see {ref}`instances-limit-units`) or in IOPS (must be suffixed with `iops`).
// See also {ref}`storage-configure-io`.
Expand Down Expand Up @@ -420,6 +431,10 @@ func (d *disk) validateConfig(instConf instance.ConfigReader) error {
return fmt.Errorf(`Cannot use both "required" and deprecated "optional" properties at the same time`)
}

if d.config["source.snapshot"] != "" && (d.config["pool"] == "" || d.config["path"] == "/") {
return fmt.Errorf(`"source.snapshot" can only be used on storage volume disk devices`)
}

if d.config["source.type"] != "" && d.config["pool"] == "" {
return fmt.Errorf(`"source.type" can only be used on storage volume disk devices`)
}
Expand Down Expand Up @@ -754,7 +769,12 @@ func (d *disk) Register() error {
return err
}

_, err = d.pool.MountInstance(diskInst, nil)
if d.config["source.snapshot"] != "" {
_, err = d.pool.MountInstanceSnapshot(diskInst, nil)
} else {
_, err = d.pool.MountInstance(diskInst, nil)
}

if err != nil {
return err
}
Expand Down Expand Up @@ -902,7 +922,7 @@ func (d *disk) startContainer() (*deviceConfig.RunConfig, error) {
}

options := []string{}
if isReadOnly {
if isReadOnly || d.config["source.snapshot"] != "" {
options = append(options, "ro")
}

Expand Down Expand Up @@ -1208,7 +1228,7 @@ func (d *disk) startVM() (*deviceConfig.RunConfig, error) {
mount.Opts = append(mount.Opts, d.detectVMPoolMountOpts()...)
}

if shared.IsTrue(d.config["readonly"]) {
if shared.IsTrue(d.config["readonly"]) || d.config["source.snapshot"] != "" {
mount.Opts = append(mount.Opts, "ro")
}

Expand Down Expand Up @@ -1624,6 +1644,7 @@ func (w *cgroupWriter) Set(version cgroup.Backend, controller string, key string
// - d.config["pool"] : pool name
// - d.config["source"] : volume name
// - d.config["source.type"] : volume type
// - d.config["source.snapshot"] : snapshot name
//
// Returns the mount path and MountInfo struct. If d.inst type is container the
// volume will be shifted if needed.
Expand Down Expand Up @@ -1651,12 +1672,23 @@ func (d *disk) mountPoolVolume() (func(), string, *storagePools.MountInfo, error
return nil, "", nil, err
}

mountInfo, err = d.pool.MountInstance(diskInst, nil)
if d.config["source.snapshot"] != "" {
mountInfo, err = d.pool.MountInstanceSnapshot(diskInst, nil)
} else {
mountInfo, err = d.pool.MountInstance(diskInst, nil)
}

if err != nil {
return nil, "", nil, err
}

revert.Add(func() { _ = d.pool.UnmountInstance(diskInst, nil) })
revert.Add(func() {
if d.config["source.snapshot"] != "" {
_ = d.pool.UnmountInstanceSnapshot(diskInst, nil)
} else {
_ = d.pool.UnmountInstance(diskInst, nil)
}
})
} else {
mountInfo, err = d.pool.MountCustomVolume(storageProjectName, volumeName, nil)
if err != nil {
Expand Down Expand Up @@ -2155,7 +2187,11 @@ func (d *disk) postStop() error {
return err
}

err = d.pool.UnmountInstance(diskInst, nil)
if d.config["source.snapshot"] != "" {
err = d.pool.UnmountInstanceSnapshot(diskInst, nil)
} else {
err = d.pool.UnmountInstance(diskInst, nil)
}
} else {
_, err = d.pool.UnmountCustomVolume(storageProjectName, volumeName, nil)
}
Expand Down