Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e8153ea
feat(agent): add LVM on file-backed loop devices (spec.fileDevices)
duckhawk Jun 23, 2026
b21774f
fix(agent): idempotent fileDevices provisioning with rollback and own…
duckhawk Jun 23, 2026
7c51bb8
fix(agent): ReattachFileDevices returns error and aborts VG activatio…
duckhawk Jun 23, 2026
d28be98
refactor(agent): read loop backing file via Commands.GetLoopBackingFile
duckhawk Jun 23, 2026
ca49513
fix(crd): make fileDevices entries immutable and symmetrize omitempty
duckhawk Jun 23, 2026
af2dca3
test(agent): cover fileDevices provisioning, cleanup, reattach and na…
duckhawk Jun 23, 2026
ec49aec
style(templates): add trailing newline to NodeGroupConfiguration shell
duckhawk Jun 23, 2026
9eef67a
style(agent): use fmt.Fprintf in validateFileDevice (staticcheck QF1012)
duckhawk Jun 23, 2026
0dadc50
fix(agent): address fileDevices code review findings
duckhawk Jun 24, 2026
4fd9530
changes in helm lib
duckhawk Jun 24, 2026
7900037
fix(crd): исправлена CEL-валидация неизменности fileDevices
duckhawk Jun 24, 2026
654a121
fix(agent): добавить импорт time в main.go
duckhawk Jun 24, 2026
8b1648a
changes in helm lib
duckhawk Jun 24, 2026
c860f65
fix(agent): address review findings for file-backed loop devices
duckhawk Jun 27, 2026
24bec6a
fix(agent): address review findings for file-backed loop devices
duckhawk Jun 27, 2026
df653e1
fix(agent): handle file-only LVMVolumeGroup with nil blockDeviceSelector
duckhawk Jun 27, 2026
c3a35bc
feat(agent): auto-create fileDevices backing directory (mkdir -p)
duckhawk Jun 27, 2026
5d4cd78
fix(agent): propagate fileDevices in LVGSet and resolve aliased loop …
duckhawk Jun 27, 2026
6dff8e5
fix(agent): include fileDevices in thin-pool sizing and record canoni…
duckhawk Jun 27, 2026
23487e9
fix(agent): harden file-device provisioning, reattach, and discovery
duckhawk Jun 27, 2026
2a18c59
fix(agent): count new file devices in update thin-pool sizing and har…
duckhawk Jun 27, 2026
4493171
refactor(agent): extract shared detached rollback context for file de…
duckhawk Jun 27, 2026
2ba1e12
fix(agent): harden file-backed loop device handling
duckhawk Jun 27, 2026
3dc8ba0
fix(agent): treat lvm "matches existing size" as a no-op thin-pool re…
duckhawk Jun 28, 2026
bbddb12
fix(agent): accept "100%"-sized thin pool in create-time validation
duckhawk Jun 28, 2026
1ff10e0
fix(agent): harden file-backed loop device handling from review
duckhawk Jun 28, 2026
5237124
fix(agent): prevent file-device VG corruption from create rollback
duckhawk Jun 28, 2026
226b450
fix(agent): address re-review findings for file-backed devices
duckhawk Jun 28, 2026
0d0e4bc
fix(agent): guard node disk and enable loop direct-I/O for file devices
duckhawk Jun 30, 2026
7e40ca4
feat(agent): confine file-device backing files to a configurable base…
duckhawk Jun 30, 2026
519a5e2
fix(agent): escalate stuck alias-PV resolution and document space rec…
duckhawk Jul 1, 2026
90e1367
test(e2e): add file-backed devices (spec.fileDevices) e2e scenarios
duckhawk Jul 1, 2026
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
30 changes: 23 additions & 7 deletions api/v1alpha1/lvm_volume_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,18 @@ type LVMVolumeGroup struct {

// +k8s:deepcopy-gen=true
type LVMVolumeGroupSpec struct {
ActualVGNameOnTheNode string `json:"actualVGNameOnTheNode"`
BlockDeviceSelector *metav1.LabelSelector `json:"blockDeviceSelector"`
ThinPools []LVMVolumeGroupThinPoolSpec `json:"thinPools"`
Type string `json:"type"`
Local LVMVolumeGroupLocalSpec `json:"local"`
ActualVGNameOnTheNode string `json:"actualVGNameOnTheNode"`
BlockDeviceSelector *metav1.LabelSelector `json:"blockDeviceSelector,omitempty"`
ThinPools []LVMVolumeGroupThinPoolSpec `json:"thinPools"`
Type string `json:"type"`
Local LVMVolumeGroupLocalSpec `json:"local"`
FileDevices []LVMVolumeGroupFileDeviceSpec `json:"fileDevices,omitempty"`
}

// +k8s:deepcopy-gen=true
type LVMVolumeGroupFileDeviceSpec struct {
Directory string `json:"directory"`
Size resource.Quantity `json:"size"`
}

// +k8s:deepcopy-gen=true
Expand Down Expand Up @@ -73,8 +80,17 @@ type LVMVolumeGroupDevice struct {

// +k8s:deepcopy-gen=true
type LVMVolumeGroupNode struct {
Devices []LVMVolumeGroupDevice `json:"devices"`
Name string `json:"name"`
Devices []LVMVolumeGroupDevice `json:"devices"`
FileDevices []LVMVolumeGroupFileDevice `json:"fileDevices,omitempty"`
Name string `json:"name"`
}

// +k8s:deepcopy-gen=true
type LVMVolumeGroupFileDevice struct {
FilePath string `json:"filePath"`
LoopDevice string `json:"loopDevice"`
Size resource.Quantity `json:"size"`
PVUuid string `json:"pvUUID"`
}

// +k8s:deepcopy-gen=true
Expand Down
11 changes: 6 additions & 5 deletions api/v1alpha1/lvm_volume_group_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ type LVMVolumeGroupSetSpec struct {

// +k8s:deepcopy-gen=true
type LVMVolumeGroupTemplate struct {
Metadata LVMVolumeGroupTemplateMeta `json:"metadata"`
BlockDeviceSelector *metav1.LabelSelector `json:"blockDeviceSelector"`
ActualVGNameOnTheNode string `json:"actualVGNameOnTheNode"`
ThinPools []LVMVolumeGroupThinPoolSpec `json:"thinPools"`
Type string `json:"type"`
Metadata LVMVolumeGroupTemplateMeta `json:"metadata"`
BlockDeviceSelector *metav1.LabelSelector `json:"blockDeviceSelector,omitempty"`
ActualVGNameOnTheNode string `json:"actualVGNameOnTheNode"`
ThinPools []LVMVolumeGroupThinPoolSpec `json:"thinPools"`
Type string `json:"type"`
FileDevices []LVMVolumeGroupFileDeviceSpec `json:"fileDevices,omitempty"`
}

// +k8s:deepcopy-gen=true
Expand Down
53 changes: 53 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 85 additions & 1 deletion crds/lvmvolumegroup.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ spec:
- rule: |
(self.type == "Local" && has(self.local)) || self.type != "Local"
message: "The 'local' field is required when the 'type' field is 'Local'."
- rule: |
has(self.blockDeviceSelector) || (has(self.fileDevices) && size(self.fileDevices) > 0)
message: "At least one of 'blockDeviceSelector' or 'fileDevices' must be specified."
- rule: '!has(oldSelf.fileDevices) || oldSelf.fileDevices.all(o, has(self.fileDevices) && self.fileDevices.exists(n, n.directory == o.directory && n.size == o.size))'
message: 'Existing fileDevices entries are immutable. Add a new entry instead of editing directory or size.'
required:
- type
- blockDeviceSelector
- actualVGNameOnTheNode
properties:
type:
Expand Down Expand Up @@ -120,6 +124,60 @@ spec:
x-kubernetes-validations:
- rule: self == oldSelf
message: "The actualVGNameOnTheNode field is immutable."
fileDevices:
type: array
maxItems: 32
x-kubernetes-list-type: map
x-kubernetes-list-map-keys:
- directory
- size
description: |
File-backed block devices to create as LVM PVs for this Volume Group.

The agent will create a preallocated file in the specified directory,
attach it as a loop device, and use it as a Physical Volume.
File devices and block devices can be used together in the same VG.

Existing entries are immutable: `directory` and `size` cannot be changed
after creation, because the backing file's path is derived from them and
the loop device captures its size at attach time. To increase capacity,
add a new `fileDevices` entry instead of editing an existing one.
items:
type: object
required:
- directory
- size
properties:
directory:
type: string
maxLength: 4096
description: |
Absolute directory on the node's filesystem where the backing file
will be created.

Must be the module's configured base directory
(fileDevicesDirectory, default /opt/deckhouse/sds/file-devices) or a
subdirectory of it; the agent rejects paths outside that subtree so a
stray entry cannot fill an arbitrary host path.

The agent creates the directory automatically (mkdir -p) if it does
not exist. The path must be absolute and must not contain '..'
segments. Provisioning fails if the path is on a read-only
filesystem, a non-directory component is in the way, or there is not
enough free space for the requested size.
size:
type: string
maxLength: 32
pattern: '^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$'
description: |
Size of the preallocated backing file. Must be at least 1Gi.

Prefer binary units (`Gi`, `Mi`). Decimal units are accepted
by the format but are smaller than they look: `1G` is 10^9 bytes,
which is below the 1Gi (2^30 bytes) minimum and will be rejected
by the agent. Because an existing entry's `size` is immutable and
cannot be removed, a too-small value can only be corrected by
deleting and recreating the LVMVolumeGroup, so set it correctly.
thinPools:
type: array
description: |
Expand Down Expand Up @@ -312,6 +370,32 @@ spec:
type: string
description: |
Name of the corresponding block device resource.
fileDevices:
type: array
description: |
Information about the file-backed devices used in the Volume Group on the current node.
items:
type: object
properties:
filePath:
type: string
description: |
Path to the backing file on the node.
loopDevice:
type: string
description: |
Loop device path (e.g., `/dev/loop0`).
size:
type: string
description: |
Size of the Physical Volume created on the backing file.
This is slightly smaller than the requested `spec.fileDevices[].size`
because LVM reserves space for PV metadata (the same way
`status.nodes[].devices[].pvSize` differs from the raw device size).
pvUUID:
type: string
description: |
LVM Physical Volume UUID.
subresources:
status: {}
additionalPrinterColumns:
Expand Down
49 changes: 48 additions & 1 deletion crds/lvmvolumegroupset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,15 @@ spec:
type: object
description: |
Common template for LVMVolumeGroup resources provided by the set.
x-kubernetes-validations:
- rule: |
has(self.blockDeviceSelector) || (has(self.fileDevices) && size(self.fileDevices) > 0)
message: "At least one of 'blockDeviceSelector' or 'fileDevices' must be specified."
- rule: '!has(oldSelf.fileDevices) || oldSelf.fileDevices.all(o, has(self.fileDevices) && self.fileDevices.exists(n, n.directory == o.directory && n.size == o.size))'
message: 'Existing fileDevices entries are immutable. Add a new entry instead of editing directory or size.'
required:
- type
- actualVGNameOnTheNode
- blockDeviceSelector
properties:
blockDeviceSelector:
type: object
Expand Down Expand Up @@ -162,6 +167,48 @@ spec:
x-kubernetes-validations:
- rule: self == oldSelf
message: "The actualVGNameOnTheNode field is immutable."
fileDevices:
type: array
maxItems: 32
x-kubernetes-list-type: map
x-kubernetes-list-map-keys:
- directory
- size
description: |
File-backed block devices to create as LVM PVs in LVMVolumeGroups.

The agent will create a preallocated file in the specified directory,
attach it as a loop device, and use it as a Physical Volume.

Existing entries are immutable: `directory` and `size` cannot be changed
after creation. To increase capacity, add a new entry instead.

> Note that the configuration will be common for every LVMVolumeGroup created by the set.
items:
type: object
required:
- directory
- size
properties:
directory:
type: string
maxLength: 4096
description: |
Absolute directory on the node's filesystem where the backing file
will be created. The agent creates the directory automatically
(mkdir -p) if it does not exist; the path must be absolute and
must not contain '..' segments.
size:
type: string
maxLength: 32
pattern: '^[0-9]+(\.[0-9]+)?(E|P|T|G|M|k|Ei|Pi|Ti|Gi|Mi|Ki)?$'
description: |
Size of the preallocated backing file. Must be at least 1Gi.

Prefer binary units (`Gi`, `Mi`). Decimal units are accepted
by the format but are smaller than they look: `1G` is 10^9 bytes,
which is below the 1Gi (2^30 bytes) minimum and will be rejected
by the agent.
thinPools:
type: array
description: |
Expand Down
41 changes: 41 additions & 0 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,47 @@ If the Volume Group has active Logical Volumes, perform the following steps:

If necessary, the command can be added to the `cloud-init` script for automatic execution when creating virtual machines.

## How do file-backed devices (fileDevices) work?

File-backed devices allow you to allocate part of an existing filesystem for LVM without dedicated block devices. The agent creates a preallocated file in the specified directory, attaches it as a loop device via `losetup`, and uses it as an LVM Physical Volume.

### Limitations

- **Confined to a base directory**: Each `directory` must be the `fileDevicesDirectory` module setting (default `/opt/deckhouse/sds/file-devices`) or a subdirectory of it. Paths outside this subtree are rejected so an arbitrary host path cannot be filled up. Point `fileDevicesDirectory` at a dedicated data disk to use a different location.
- **Directory auto-created**: The agent creates the backing directory automatically (`mkdir -p`) on the node if it does not exist. The path must be absolute and free of `..` segments; provisioning fails only if the path is on a read-only filesystem or a non-directory component is in the way.
- **No resize**: File device size cannot be changed after creation. To increase capacity, add a new `fileDevices` entry.
- **Preallocated only**: Files are created with `fallocate`, which preallocates space on the filesystem. The agent refuses to create a backing file larger than the directory's free space, so a too-large entry is reported on the resource instead of filling the node.
- **Minimum size**: Each file device must be at least 1Gi.
- **Performance overhead**: LVM on a loop device over a filesystem adds double indirection. Use file-backed devices only when dedicated disks are not available.
- **Host LVM filter**: NodeGroupConfiguration adds loop devices to the host-wide LVM `global_filter`, so unprivileged `lvm`/`pvs` on the node do not see them. The agent re-attaches managed backing files at startup and uses its own LVM config for managed Volume Groups.

### Reclaiming space

Backing files are preallocated with `fallocate`, so a file occupies its full size on the node's filesystem from the moment it is created and does **not** grow or shrink automatically as data is written or deleted inside the volume.

Space can still be returned to the node's filesystem through the discard (TRIM) chain, which works end to end for these devices:

`filesystem on the volume` → `thin LV` → `thin pool` (created with `discards=passdown` by default) → `/dev/loopN` → backing file.

A loop device translates discards into `FALLOC_FL_PUNCH_HOLE` on its backing file, turning the reclaimed regions into holes and making the file sparse. To trigger reclamation after deleting data:

- run `fstrim <mountpoint>` on the volume's filesystem periodically (for example via the `fstrim.timer` systemd unit), or
- mount the volume with `-o discard` for continuous (online) discard.

Caveats:

- Only whole thin-pool chunks are reclaimed, so the effect depends on the pool's chunk size and alignment.
- Snapshots pin the chunks they reference, so space shared with a snapshot is not freed until the snapshot is removed.
- The backing file only shrinks after a write → delete → trim cycle; a freshly created file always occupies its full preallocated size.

### Reboot recovery

After a node reboot, the agent automatically re-establishes loop device mappings before activating Volume Groups. The backing file path is stored in `status.nodes[].fileDevices[].filePath`.

### Deletion

When an LVMVolumeGroup with file-backed devices is deleted, the agent detaches the loop devices and removes the backing files.

## What labels are added by the controller to BlockDevice resources?

- `status.blockdevice.storage.deckhouse.io/type`: LVM type.
Expand Down
Loading
Loading