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

Attach VM root volumes as disk devices #14532

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6b1ba3b
api: Add `instance_root_volume_attachment`
MggMuggins Dec 2, 2024
f3578ee
doc/explanation/storage: Fix wording
MggMuggins Nov 6, 2024
02b5291
lxd/storage: Allow `security.shared` for virtual-machine volumes
MggMuggins Nov 6, 2024
0ab5c40
lxd/instance/instancetype: Update instance config key docs
MggMuggins Nov 19, 2024
0a167e0
doc: `make update-metadata`
MggMuggins Nov 6, 2024
56966ea
lxd/storage: Allow parsing virtual-machine/* volumes as disk sources
MggMuggins Nov 20, 2024
2fb9f07
lxd/device/disk: Use correct storage volume name
MggMuggins Nov 25, 2024
5b18c5e
lxc: Allow virtual-machine volumes in `storage volume attach`
MggMuggins Nov 20, 2024
57449d7
lxc/storage_volume: Parse source during detach
MggMuggins Dec 2, 2024
2a8fb8d
lxd/storage: Detect root disk devices when determining if a volume is…
MggMuggins Nov 20, 2024
5f11337
lxd/device/disk: Allow vm root attachments with security.protection.s…
MggMuggins Nov 21, 2024
10abbd2
lxd/device/disk: Prevent instances attaching their own root volumes
MggMuggins Dec 11, 2024
ea5dd7a
lxd: Correctly report vm volume used-by
MggMuggins Nov 21, 2024
5821d03
lxd/instance/drivers: Prevent removing security.protection.start...
MggMuggins Nov 22, 2024
c756034
lxd/storage: Refactor security.shared check
MggMuggins Nov 22, 2024
d051bdc
lxd/storage: Check disabling security.shared on virtual-machine volumes
MggMuggins Nov 22, 2024
c6a5310
lxd: Prevent instance delete if root volume is in use
MggMuggins Nov 23, 2024
70febb6
doc: Add root volume attachment to storage volume how-to
MggMuggins Dec 4, 2024
a4ebc1e
doc: `make i18n`
MggMuggins Dec 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/.custom_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ uptime
URI
URIs
userspace
UUIDs
vCPU
vCPUs
VDPA
Expand Down
6 changes: 6 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2538,3 +2538,9 @@ Adds a new {config:option}`device-unix-hotplug-device-conf:subsystem` configurat

## `storage_ceph_osd_pool_size`
This introduces the configuration keys {config:option}`storage-ceph-pool-conf:ceph.osd.pool_size`, and {config:option}`storage-cephfs-pool-conf:cephfs.osd_pool_size` to be used when adding or updating a `ceph` or `cephfs` storage pool to instruct LXD to create set the replication size for the underlying OSD pools.

## `instance_root_volume_attachment`

Adds support for instance root volumes to be attached to other instances as disk
Comment on lines +2542 to +2544
Copy link
Contributor

Choose a reason for hiding this comment

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

If I unserstood the scope of this PR correctly, this should state virtual machines instead of instances as container volumes won't be available for attaching.

devices. Introduces the `<type>/<volume>` syntax for the `source` property of
disk devices.
8 changes: 4 additions & 4 deletions doc/explanation/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ Storage volumes can be of the following types:

`container`/`virtual-machine`
: LXD automatically creates one of these storage volumes when you launch an instance.
It is used as the root disk for the instance, and it is destroyed when the instance is deleted.
It is used as the root disk for the instance and is destroyed when the instance is deleted.

This storage volume is created in the storage pool that is specified in the profile used when launching the instance (or the default profile, if no profile is specified).
The storage pool can be explicitly specified by providing the `--storage` flag to the launch command.
The storage pool can be explicitly specified by providing the `--storage` flag to the {ref}`launch command <lxc_launch.md>`.
If no pool or profile is specified, LXD uses the storage pool of the default profile's root disk device.

`image`
: LXD automatically creates one of these storage volumes when it unpacks an image to launch one or more instances from it.
Expand Down Expand Up @@ -157,7 +157,7 @@ Each storage volume uses one of the following content types:

Custom storage volumes of content type `block` can only be attached to virtual machines.
By default, they can only be attached to one instance at a time, because simultaneous access can lead to data corruption.
Sharing a custom storage volumes of content type `block` is made possible through the usage of the `security.shared` configuration key.
Sharing custom storage volumes of content type `block` is made possible through the usage of the `security.shared` configuration key.

`iso`
: This content type is used for custom ISO volumes.
Expand Down
28 changes: 28 additions & 0 deletions doc/howto/storage_volumes.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,34 @@ For example, to set the default volume size for `my-pool`, use the following com

lxc storage set my-pool volume.size=15GiB

## Attach instance root volumes to other instances
Virtual-machine root volumes can be attached as disk devices to other virtual machines.
In order to prevent concurrent access, `security.protection.start` must be set on
an instance before its root volume can be attached to another virtual-machine.

```{caution}
Because instances created from the same image share the same partition and file system
UUIDs and labels, booting an instance with two root file systems mounted may result
in the wrong root file system being used. This may result in unexpected behavior
or data loss. **It is strongly recommended to only attach virtual-machine root
volumes to other virtual machines when the target virtual-machine is running.**
```

Assuming `vm1` is stopped and `vm2` is running, attach the `virtual-machine/vm1` storage
volume to `vm2`:

lxc config set vm1 security.protection.start=true
lxc storage volume attach my-pool virtual-machine/vm1 vm2

`virtual-machine/vm1` must be detached from `vm2` before `security.protection.start`
can be unset from `vm1`:

lxc storage volume detach my-pool virtual-machine/vm1 vm2
lxc config unset vm1 security.protection.start

`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.

## Resize a storage volume

If you need more storage in a volume, you can increase the size of your storage volume.
Expand Down
16 changes: 8 additions & 8 deletions doc/metadata.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2193,7 +2193,7 @@ See {ref}`container-security` for more information.

```{config:option} security.protection.delete instance-security
:defaultdesc: "`false`"
:liveupdate: "yes"
:liveupdate: "container"
:shortdesc: "Whether to prevent the instance from being deleted"
:type: "bool"

Expand All @@ -2210,7 +2210,7 @@ Set this option to `true` to prevent the instance's file system from being UID/G

```{config:option} security.protection.start instance-security
:defaultdesc: "`false`"
:liveupdate: "yes"
:liveupdate: "container"
:shortdesc: "Whether to prevent the instance from being started"
:type: "bool"

Expand Down Expand Up @@ -4881,7 +4881,7 @@ prior to creating the storage pool.
<!-- config group storage-btrfs-pool-conf end -->
<!-- config group storage-btrfs-volume-conf start -->
```{config:option} security.shared storage-btrfs-volume-conf
:condition: "custom block volume"
:condition: "virtual-machine or custom block volume"
:defaultdesc: "same as `volume.security.shared` or `false`"
:scope: "global"
:shortdesc: "Enable volume sharing"
Expand Down Expand Up @@ -5073,7 +5073,7 @@ If not set, `ext4` is assumed.
```

```{config:option} security.shared storage-ceph-volume-conf
:condition: "custom block volume"
:condition: "virtual-machine or custom block volume"
:defaultdesc: "same as `volume.security.shared` or `false`"
:scope: "global"
:shortdesc: "Enable volume sharing"
Expand Down Expand Up @@ -5404,7 +5404,7 @@ to be placed on the socket I/O.
<!-- config group storage-dir-pool-conf end -->
<!-- config group storage-dir-volume-conf start -->
```{config:option} security.shared storage-dir-volume-conf
:condition: "custom block volume"
:condition: "virtual-machine or custom block volume"
:defaultdesc: "same as `volume.security.shared` or `false`"
:scope: "global"
:shortdesc: "Enable volume sharing"
Expand Down Expand Up @@ -5622,7 +5622,7 @@ The size must be at least 4096 bytes, and a multiple of 512 bytes.
```

```{config:option} security.shared storage-lvm-volume-conf
:condition: "custom block volume"
:condition: "virtual-machine or custom block volume"
:defaultdesc: "same as `volume.security.shared` or `false`"
:scope: "global"
:shortdesc: "Enable volume sharing"
Expand Down Expand Up @@ -5832,7 +5832,7 @@ If not set, `ext4` is assumed.
```

```{config:option} security.shared storage-powerflex-volume-conf
:condition: "custom block volume"
:condition: "virtual-machine or custom block volume"
:defaultdesc: "same as `volume.security.shared` or `false`"
:scope: "global"
:shortdesc: "Enable volume sharing"
Expand Down Expand Up @@ -6003,7 +6003,7 @@ If not set, `ext4` is assumed.
```

```{config:option} security.shared storage-zfs-volume-conf
:condition: "custom block volume"
:condition: "virtual-machine or custom block volume"
:defaultdesc: "same as `volume.security.shared` or `false`"
:scope: "global"
:shortdesc: "Enable volume sharing"
Expand Down
42 changes: 22 additions & 20 deletions lxc/storage_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,12 @@ type cmdStorageVolumeAttach struct {

func (c *cmdStorageVolumeAttach) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("attach", i18n.G("[<remote>:]<pool> <volume> <instance> [<device name>] [<path>]"))
cmd.Use = usage("attach", i18n.G("[<remote>:]<pool> [<type>/]<volume> <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`))
`Attach new storage volumes to instances

<type> must be one of "custom" or "virtual-machine"`))

cmd.RunE = c.run

Expand Down Expand Up @@ -210,8 +212,8 @@ func (c *cmdStorageVolumeAttach) run(cmd *cobra.Command, args []string) error {
}

volName, volType := parseVolume("custom", args[1])
if volType != "custom" {
return errors.New(i18n.G("Only \"custom\" volumes can be attached to instances"))
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
Expand Down Expand Up @@ -257,7 +259,7 @@ func (c *cmdStorageVolumeAttach) run(cmd *cobra.Command, args []string) error {
device := map[string]string{
"type": "disk",
"pool": resource.name,
"source": volName,
"source": args[1],
Copy link
Contributor

Choose a reason for hiding this comment

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

what if the user provides just the volume name without the type?

"path": devPath,
}

Expand All @@ -279,10 +281,12 @@ type cmdStorageVolumeAttachProfile struct {

func (c *cmdStorageVolumeAttachProfile) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("attach-profile", i18n.G("[<remote:>]<pool> <volume> <profile> [<device name>] [<path>]"))
cmd.Use = usage("attach-profile", i18n.G("[<remote:>]<pool> [<type>/]<volume> <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`))
`Attach new storage volumes to profiles

<type> must be one of "custom" or "virtual-machine"`))

cmd.RunE = c.run

Expand Down Expand Up @@ -340,8 +344,8 @@ func (c *cmdStorageVolumeAttachProfile) run(cmd *cobra.Command, args []string) e
}

volName, volType := parseVolume("custom", args[1])
if volType != "custom" {
return errors.New(i18n.G("Only \"custom\" volumes can be attached to instances"))
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
Expand All @@ -354,7 +358,7 @@ func (c *cmdStorageVolumeAttachProfile) run(cmd *cobra.Command, args []string) e
device := map[string]string{
"type": "disk",
"pool": resource.name,
"source": vol.Name,
"source": args[1],
}

// Ignore path for block volumes
Expand Down Expand Up @@ -803,7 +807,7 @@ type cmdStorageVolumeDetach struct {

func (c *cmdStorageVolumeDetach) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("detach", i18n.G("[<remote>:]<pool> <volume> <instance> [<device name>]"))
cmd.Use = usage("detach", i18n.G("[<remote>:]<pool> [<type>/]<volume> <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 @@ -861,14 +865,13 @@ func (c *cmdStorageVolumeDetach) run(cmd *cobra.Command, args []string) error {
}

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

// Find the device
if devName == "" {
for n, d := range inst.Devices {
if d["type"] == "disk" && d["pool"] == resource.name && d["source"] == volName {
sourceType, sourceName := parseVolume("custom", d["source"])

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

func (c *cmdStorageVolumeDetachProfile) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("detach-profile", i18n.G("[<remote:>]<pool> <volume> <profile> [<device name>]"))
cmd.Use = usage("detach-profile", i18n.G("[<remote:>]<pool> [<type>/]<volume> <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 @@ -963,14 +966,13 @@ func (c *cmdStorageVolumeDetachProfile) run(cmd *cobra.Command, args []string) e
}

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

// Find the device
if devName == "" {
for n, d := range profile.Devices {
if d["type"] == "disk" && d["pool"] == resource.name && d["source"] == volName {
sourceType, sourceName := parseVolume("custom", d["source"])
MggMuggins marked this conversation as resolved.
Show resolved Hide resolved

if d["type"] == "disk" && d["pool"] == resource.name && volType == sourceType && volName == sourceName {
if devName != "" {
return errors.New(i18n.G("More than one device matches, specify the device name"))
}
Expand Down
47 changes: 36 additions & 11 deletions lxd/device/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func (d *disk) checkBlockVolSharing(instanceType instancetype.Type, projectName
}

if instanceType == instancetype.Any {
return fmt.Errorf("Cannot add custom storage block volume to profiles if security.shared is false or unset")
return fmt.Errorf("Cannot add block volume to profiles if security.shared is false or unset")
}

err := storagePools.VolumeUsedByInstanceDevices(d.state, d.pool.Name(), projectName, volume, true, func(inst db.InstanceArgs, project api.Project, usedByDevices []string) error {
Expand All @@ -164,13 +164,23 @@ func (d *disk) checkBlockVolSharing(instanceType instancetype.Type, projectName
return nil
}

return db.ErrListStop
})
if err != nil {
if err == db.ErrListStop {
return fmt.Errorf("Cannot add custom storage block volume to more than one instance if security.shared is false or unset")
// Don't count a VM volume's instance if security.protection.start is preventing that instance from starting
if volume.Type == cluster.StoragePoolVolumeTypeNameVM && volume.Project == inst.Project && volume.Name == inst.Name {
apiInst, err := inst.ToAPI()
if err != nil {
return err
}

apiInst.ExpandedConfig = instancetype.ExpandInstanceConfig(d.state.GlobalConfig.Dump(), apiInst.Config, inst.Profiles)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we could put a comment here explaining why security.protection.start makes it okay to attach the root volume on another instance.

if shared.IsTrue(apiInst.ExpandedConfig["security.protection.start"]) {
return nil
}
Comment on lines +167 to +178
Copy link
Contributor

Choose a reason for hiding this comment

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

Good idea here with security.protection.start!

If security.protection.start is set on the instance using the volume as an additional drive instead of the one using it as root, it should also be okay to attach the volume to it. Am I correct?

I am wondering now if we should also allow custom block volumes to be attached to more than one instance if .security.protection.start is set for all but one of them. If so, it would be nice to keep those consistent I think.

}

return fmt.Errorf("Cannot add block volume to more than one instance if security.shared is false or unset")
})
if err != nil {
return err
}

Expand Down Expand Up @@ -468,6 +478,17 @@ func (d *disk) validateConfig(instConf instance.ConfigReader) error {
return err
}

if d.inst != nil {
instVolType, err := storagePools.InstanceTypeToVolumeType(d.inst.Type())
if err != nil {
return err
}

if instVolType == volumeType && d.inst.Name() == volumeName {
return errors.New("Instance root device cannot be attached to itself")
}
}

// Derive the effective storage project name from the instance config's project.
storageProjectName, err = project.StorageVolumeProject(d.state.DB.Cluster, instConf.Project().Name, dbVolumeType)
if err != nil {
Expand Down Expand Up @@ -1579,9 +1600,6 @@ func (d *disk) mountPoolVolume() (func(), string, *storagePools.MountInfo, error
return nil, "", nil, err
}

volStorageName := project.StorageVolume(storageProjectName, volumeName)
srcPath := storageDrivers.GetVolumeMountPath(d.config["pool"], volumeType, volStorageName)

mountInfo, err = d.pool.MountVolume(storageProjectName, volumeName, volumeType, nil)
if err != nil {
return nil, "", nil, fmt.Errorf(`Failed mounting storage volume "%s/%s" from storage pool %q: %w`, volumeTypeName, volumeName, d.pool.Name(), err)
Expand All @@ -1600,6 +1618,15 @@ func (d *disk) mountPoolVolume() (func(), string, *storagePools.MountInfo, error
return nil, "", nil, fmt.Errorf("Failed to fetch local storage volume record: %w", err)
}

var volStorageName string
if dbVolume.Type == cluster.StoragePoolVolumeTypeNameCustom {
volStorageName = project.StorageVolume(storageProjectName, volumeName)
} else {
volStorageName = project.Instance(storageProjectName, volumeName)
}

srcPath := storageDrivers.GetVolumeMountPath(d.config["pool"], volumeType, volStorageName)

if d.inst.Type() == instancetype.Container {
if dbVolume.ContentType != cluster.StoragePoolVolumeContentTypeNameFS {
return nil, "", nil, fmt.Errorf("Only filesystem volumes are supported for containers")
Expand All @@ -1612,8 +1639,6 @@ func (d *disk) mountPoolVolume() (func(), string, *storagePools.MountInfo, error
}

if dbVolume.ContentType == cluster.StoragePoolVolumeContentTypeNameBlock || dbVolume.ContentType == cluster.StoragePoolVolumeContentTypeNameISO {
volStorageName := project.StorageVolume(storageProjectName, volumeName)

volume := d.pool.GetVolume(volumeType, storageDrivers.ContentType(dbVolume.ContentType), volStorageName, dbVolume.Config)

srcPath, err = d.pool.Driver().GetVolumeDiskPath(volume)
Expand Down
Loading
Loading