debian-forge-composer/internal/osbuild2/device.go
Christian Kellner 9e5b265a58 osbuild2: lock loopback devices during sfdisk, mkfs
Since udev will probe block devices it is advisable to hold a lock
on the device when modifying its partition table or the superblock
of the filesystem (see [1]). osbuild loopback devices do support
this via the `lock` option. Set this option for all operation that
involve changing block device "metadata" that could potentionally
race with udev, such as sfdisk, mkfs, creating a luks2 container
and creating LVM2 volume groups and logical volumes.
NB: osbuild also has its own device inhibition logic to prevent
udev/lvm2 from auto activating devices and in general to limit the
interaction between the host and devices used by osbuild. See [2]
for more information.
NB: this also locks the loopback device in situation where we the
it is strickly not the right thing to do, e.g. when creating a fs
on a logical voume that is located on a loopback device, since in
this case the device we would need to lock is the logical volume.
Sadly, LVM/DM devices are exempt from block device locking. But,
due to a bug in osbuild < 50, the udev inhibitor does *not* work
for loopback devices and therefore we have to use the actual lock
to preven LVM device auto-activation via `69-dm-lvm-metad.rules`.
The change was implemented by adding a new boolean to `getDevices`
indicating if the loopback device should be locked or not. Once
we depend on osbuild 50 we can change the logic in `getDevices`
to only lock the loopback device if the number of devices is one,
i.e. we are working directly on the loopback device.

[1] https://systemd.io/BLOCK_DEVICE_LOCKING/
[2] /usr/lib/udev/rules.d/10-osbuild-inhibitor.rules
2022-02-28 17:09:30 +01:00

189 lines
4.7 KiB
Go

package osbuild2
import (
"fmt"
"strings"
"github.com/osbuild/osbuild-composer/internal/disk"
)
type Devices map[string]Device
type Device struct {
Type string `json:"type"`
Parent string `json:"parent,omitempty"`
Options DeviceOptions `json:"options,omitempty"`
}
type DeviceOptions interface {
isDeviceOptions()
}
func GenDeviceCreationStages(pt *disk.PartitionTable, filename string) []*Stage {
stages := make([]*Stage, 0)
genStages := func(e disk.Entity, path []disk.Entity) error {
switch ent := e.(type) {
case *disk.LUKSContainer:
// do not include us when getting the devices
stageDevices, lastName := getDevices(path[:len(path)-1], filename, true)
// "org.osbuild.luks2.format" expects a "device" to create the VG on,
// thus rename the last device to "device"
lastDevice := stageDevices[lastName]
delete(stageDevices, lastName)
stageDevices["device"] = lastDevice
stage := NewLUKS2CreateStage(
&LUKS2CreateStageOptions{
UUID: ent.UUID,
Passphrase: ent.Passphrase,
Cipher: ent.Cipher,
Label: ent.Label,
Subsystem: ent.Subsystem,
SectorSize: ent.SectorSize,
PBKDF: Argon2id{
Method: "argon2id",
Iterations: ent.PBKDF.Iterations,
Memory: ent.PBKDF.Memory,
Parallelism: ent.PBKDF.Parallelism,
},
},
stageDevices)
stages = append(stages, stage)
case *disk.LVMVolumeGroup:
// do not include us when getting the devices
stageDevices, lastName := getDevices(path[:len(path)-1], filename, true)
// "org.osbuild.lvm2.create" expects a "device" to create the VG on,
// thus rename the last device to "device"
lastDevice := stageDevices[lastName]
delete(stageDevices, lastName)
stageDevices["device"] = lastDevice
volumes := make([]LogicalVolume, len(ent.LogicalVolumes))
for idx, lv := range ent.LogicalVolumes {
volumes[idx].Name = lv.Name
// NB: we need to specify the size in bytes, since lvcreate
// defaults to megabytes
volumes[idx].Size = fmt.Sprintf("%dB", lv.Size)
}
stage := NewLVM2CreateStage(
&LVM2CreateStageOptions{
Volumes: volumes,
}, stageDevices)
stages = append(stages, stage)
}
return nil
}
_ = pt.ForEachEntity(genStages)
return stages
}
func GenDeviceFinishStages(pt *disk.PartitionTable, filename string) []*Stage {
stages := make([]*Stage, 0)
genStages := func(e disk.Entity, path []disk.Entity) error {
switch ent := e.(type) {
case *disk.LVMVolumeGroup:
// do not include us when getting the devices
stageDevices, lastName := getDevices(path[:len(path)-1], filename, true)
// "org.osbuild.lvm2.metadata" expects a "device" to rename the VG,
// thus rename the last device to "device"
lastDevice := stageDevices[lastName]
delete(stageDevices, lastName)
stageDevices["device"] = lastDevice
stage := NewLVM2MetadataStage(
&LVM2MetadataStageOptions{
VGName: ent.Name,
}, stageDevices)
stages = append(stages, stage)
}
return nil
}
_ = pt.ForEachEntity(genStages)
return stages
}
func deviceName(p disk.Entity) string {
if p == nil {
panic("device is nil; this is a programming error")
}
switch payload := p.(type) {
case disk.Mountable:
return pathdot(payload.GetMountpoint())
case *disk.LUKSContainer:
return "luks-" + payload.UUID[:4]
case *disk.LVMVolumeGroup:
return payload.Name + "vg"
case *disk.LVMLogicalVolume:
return payload.Name
}
panic(fmt.Sprintf("unsupported device type in deviceName: '%T'", p))
}
func getDevices(path []disk.Entity, filename string, lockLoopback bool) (map[string]Device, string) {
var pt *disk.PartitionTable
do := make(map[string]Device)
parent := ""
for _, elem := range path {
switch e := elem.(type) {
case *disk.PartitionTable:
pt = e
case *disk.Partition:
if pt == nil {
panic("path does not contain partition table; this is a programming error")
}
lbopt := LoopbackDeviceOptions{
Filename: filename,
Start: pt.BytesToSectors(e.Start),
Size: pt.BytesToSectors(e.Size),
SectorSize: nil,
Lock: lockLoopback,
}
name := deviceName(e.Payload)
do[name] = *NewLoopbackDevice(&lbopt)
parent = name
case *disk.LUKSContainer:
lo := LUKS2DeviceOptions{
Passphrase: e.Passphrase,
}
name := deviceName(e.Payload)
do[name] = *NewLUKS2Device(parent, &lo)
parent = name
case *disk.LVMLogicalVolume:
lo := LVM2LVDeviceOptions{
Volume: e.Name,
}
name := deviceName(e.Payload)
do[name] = *NewLVM2LVDevice(parent, &lo)
parent = name
}
}
return do, parent
}
func pathdot(path string) string {
if path == "/" {
return "root"
}
path = strings.TrimLeft(path, "/")
return strings.ReplaceAll(path, "/", ".")
}