Skip to content

Commit 8e59fa6

Browse files
committed
feat: add LVM status controller
Signed-off-by: Mateusz Urbanek <mateusz.urbanek@siderolabs.com>
1 parent b48a2be commit 8e59fa6

26 files changed

Lines changed: 4116 additions & 190 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
syntax = "proto3";
2+
3+
package talos.resource.definitions.storage;
4+
5+
option go_package = "github.com/siderolabs/talos/pkg/machinery/api/resource/definitions/storage";
6+
option java_package = "dev.talos.api.resource.definitions.storage";
7+
8+
// LVMLogicalVolumeStatusSpec is the spec for LVMLogicalVolumeStatus resource.
9+
//
10+
// Fields mirror the colon-separated output of `lvdisplay -c`.
11+
message LVMLogicalVolumeStatusSpec {
12+
// Path is the device-mapper path of the LV, e.g. /dev/vg0/data.
13+
string path = 1;
14+
// VGName is the parent volume group name.
15+
string vg_name = 2;
16+
// Access is a bitmask of LV access flags (1=read, 2=write, 4=visible).
17+
uint64 access = 3;
18+
// Status is a bitmask of LV status flags (1=active, 2=allocated, …).
19+
uint64 status = 4;
20+
// InternalLVNumber is the historical internal LV index; always -1 in modern LVM, normalised to 0.
21+
uint64 internal_lv_number = 5;
22+
// OpenCount is the number of times the LV is currently open (mounted/held).
23+
uint64 open_count = 6;
24+
// SizeBytes is the LV size in bytes (sectors × 512 from the raw output).
25+
uint64 size_bytes = 7;
26+
// CurrentLE is the current number of logical extents allocated to the LV.
27+
uint64 current_le = 8;
28+
// AllocatedLE is the number of LEs allocated for snapshot/mirror copies; 0 for plain LVs.
29+
uint64 allocated_le = 9;
30+
// AllocPolicy is the allocation policy enum (0=normal, 1=contiguous, 2=anywhere, 3=cling, 4=inherit).
31+
uint64 alloc_policy = 10;
32+
// ReadAheadSectors is the configured read-ahead in 512-byte sectors; 0 means auto.
33+
uint64 read_ahead_sectors = 11;
34+
// Major is the kernel block-device major number for the LV.
35+
uint32 major = 12;
36+
// Minor is the kernel block-device minor number for the LV.
37+
uint32 minor = 13;
38+
}
39+
40+
// LVMPhysicalVolumeStatusSpec is the spec for LVMPhysicalVolumeStatus resource.
41+
//
42+
// Fields mirror the colon-separated output of `pvdisplay -c`.
43+
message LVMPhysicalVolumeStatusSpec {
44+
// Device is the block-device path backing the PV, e.g. /dev/sda1.
45+
string device = 1;
46+
// VGName is the parent volume group name; empty if the PV is not assigned to a VG.
47+
string vg_name = 2;
48+
// SizeBytes is the usable PV size in bytes (sectors × 512 from the raw output).
49+
uint64 size_bytes = 3;
50+
// InternalPVNumber is the historical internal PV index; always -1 in modern LVM, normalised to 0.
51+
uint64 internal_pv_number = 4;
52+
// Status is a bitmask of PV status flags (allocatable, exported, missing, …).
53+
uint64 status = 5;
54+
// Allocatable mirrors the allocatable bit from Status; non-zero means new LVs can use this PV.
55+
uint64 allocatable = 6;
56+
// CurrentLE is the number of LEs currently in use on this PV; -1 in raw output is normalised to 0.
57+
uint64 current_le = 7;
58+
// PESize is the physical-extent size in bytes (KiB × 1024); matches the parent VG.
59+
uint64 pe_size = 8;
60+
// TotalPE is the total number of physical extents on this PV.
61+
uint64 total_pe = 9;
62+
// FreePE is the number of physical extents on this PV not yet allocated.
63+
uint64 free_pe = 10;
64+
// AllocPE is the number of physical extents on this PV currently allocated to LVs.
65+
uint64 alloc_pe = 11;
66+
// UUID is the stable PV identifier written to the PV label.
67+
string uuid = 12;
68+
}
69+
70+
// LVMVolumeGroupStatusSpec is the spec for LVMVolumeGroupStatus resource.
71+
//
72+
// Fields mirror the colon-separated output of `vgdisplay -c`.
73+
message LVMVolumeGroupStatusSpec {
74+
// Name is the volume group name.
75+
string name = 1;
76+
// Access is the VG access mode string emitted by vgdisplay (e.g. "r/w", "r--").
77+
string access = 2;
78+
// Status is a bitmask of VG status flags (resizable, exported, clustered, …).
79+
uint64 status = 3;
80+
// InternalVGNumber is the historical internal VG index; always -1 in modern LVM, normalised to 0.
81+
uint64 internal_vg_number = 4;
82+
// MaxLV is the configured max number of LVs in the VG; 0 means no limit.
83+
uint64 max_lv = 5;
84+
// CurLV is the current number of LVs in the VG.
85+
uint64 cur_lv = 6;
86+
// OpenLV is the number of LVs in the VG that are currently open.
87+
uint64 open_lv = 7;
88+
// MaxLVSize is the configured per-LV size cap in sectors; 0 means no limit.
89+
uint64 max_lv_size = 8;
90+
// MaxPV is the configured max number of PVs in the VG; 0 means no limit.
91+
uint64 max_pv = 9;
92+
// CurPV is the current number of PVs in the VG.
93+
uint64 cur_pv = 10;
94+
// ActPV is the number of currently active PVs (online and accessible).
95+
uint64 act_pv = 11;
96+
// SizeBytes is the total VG size in bytes (KiB × 1024 from the raw output).
97+
uint64 size_bytes = 12;
98+
// PESize is the physical-extent size in bytes (KiB × 1024); 4 MiB by default.
99+
uint64 pe_size = 13;
100+
// TotalPE is the total number of physical extents in the VG.
101+
uint64 total_pe = 14;
102+
// AllocPE is the number of physical extents currently allocated to LVs.
103+
uint64 alloc_pe = 15;
104+
// FreePE is the number of physical extents available for new allocations.
105+
uint64 free_pe = 16;
106+
// UUID is the stable VG identifier used in LVM metadata.
107+
string uuid = 17;
108+
}
109+

hack/release.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ The DNS protocol can be configured on a per-name server basis in the `ResolverCo
114114
description = """\
115115
Talos now supports a new `ImageCacheConfig` document for configuring the Image Cache feature, replacing the old `machine.features.imageCache` field in the v1alpha1 config.
116116
Old configuration is still supported for backwards compatibility.
117+
"""
118+
119+
[notes.lvm_status]
120+
title = "LVM Status"
121+
description = """\
122+
Talos now provides detailed LVM status information, allowing for better monitoring and management of LVM volumes.
117123
"""
118124

119125
[make_deps]

internal/app/machined/pkg/controllers/block/lvm.go renamed to internal/app/machined/pkg/controllers/storage/lvm_activation.go

Lines changed: 10 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,27 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

5-
package block
5+
package storage
66

77
import (
88
"context"
99
"fmt"
10-
"strings"
1110

1211
"github.com/cosi-project/runtime/pkg/controller"
1312
"github.com/cosi-project/runtime/pkg/safe"
1413
"github.com/cosi-project/runtime/pkg/state"
1514
"github.com/hashicorp/go-multierror"
1615
"github.com/siderolabs/gen/optional"
17-
"github.com/siderolabs/go-cmd/pkg/cmd"
1816
"go.uber.org/zap"
1917

2018
machineruntime "github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
19+
"github.com/siderolabs/talos/pkg/lvm"
2120
"github.com/siderolabs/talos/pkg/machinery/constants"
2221
"github.com/siderolabs/talos/pkg/machinery/resources/block"
2322
"github.com/siderolabs/talos/pkg/machinery/resources/v1alpha1"
2423
)
2524

26-
// LVMActivationController activates LVM volumes when they are discovered by the block.DiscoveryController.
25+
// LVMActivationController activates LVM volume groups discovered by the block.DiscoveryController.
2726
type LVMActivationController struct {
2827
V1Alpha1Mode machineruntime.Mode
2928

@@ -33,7 +32,7 @@ type LVMActivationController struct {
3332

3433
// Name implements controller.Controller interface.
3534
func (ctrl *LVMActivationController) Name() string {
36-
return "block.LVMActivationController"
35+
return "storage.LVMActivationController"
3736
}
3837

3938
// Inputs implements controller.Controller interface.
@@ -161,18 +160,7 @@ func (ctrl *LVMActivationController) Run(ctx context.Context, r controller.Runti
161160

162161
logger.Info("activating LVM volume", zap.String("name", vgName))
163162

164-
// activate the volume group
165-
if _, err = cmd.RunWithOptions(
166-
ctx,
167-
"/sbin/lvm",
168-
[]string{
169-
"vgchange",
170-
"-aay",
171-
"--autoactivation",
172-
"event",
173-
vgName,
174-
},
175-
); err != nil {
163+
if err = lvm.VGChangeActivate(ctx, vgName); err != nil {
176164
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to activate LVM volume %s: %w", vgName, err))
177165
} else {
178166
ctrl.activatedVGs[vgName] = struct{}{}
@@ -185,43 +173,14 @@ func (ctrl *LVMActivationController) Run(ctx context.Context, r controller.Runti
185173
}
186174
}
187175

188-
// checkVGNeedsActivation checks if the device is part of a volume group and returns the volume group name
189-
// if it needs to be activated, otherwise it returns an empty string.
176+
// checkVGNeedsActivation checks if the device is part of a complete volume
177+
// group and returns that VG name when activation is needed; otherwise returns
178+
// an empty string. See lvmautoactivation(7).
190179
func (ctrl *LVMActivationController) checkVGNeedsActivation(ctx context.Context, devicePath string) (string, error) {
191-
// first we check if all associated volumes are available
192-
// https://man7.org/linux/man-pages/man7/lvmautoactivation.7.html
193-
stdOut, err := cmd.RunWithOptions(
194-
ctx,
195-
"/sbin/lvm",
196-
[]string{
197-
"pvscan",
198-
"--cache",
199-
"--listvg",
200-
"--checkcomplete",
201-
"--vgonline",
202-
"--autoactivation",
203-
"event",
204-
"--udevoutput",
205-
devicePath,
206-
},
207-
)
180+
udev, err := lvm.PVScanAutoActivation(ctx, devicePath)
208181
if err != nil {
209182
return "", fmt.Errorf("failed to check if LVM volume backed by device %s needs activation: %w", devicePath, err)
210183
}
211184

212-
// parse the key-value pairs from the udev output
213-
for line := range strings.SplitSeq(stdOut, "\n") {
214-
key, value, ok := strings.Cut(line, "=")
215-
if !ok {
216-
continue
217-
}
218-
219-
value = strings.Trim(value, "'\"")
220-
221-
if key == "LVM_VG_NAME_COMPLETE" {
222-
return value, nil
223-
}
224-
}
225-
226-
return "", nil
185+
return udev[lvm.UdevKeyVGNameComplete], nil
227186
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package storage
6+
7+
import (
8+
"context"
9+
"errors"
10+
"fmt"
11+
"os/exec"
12+
"strings"
13+
"time"
14+
15+
"github.com/cosi-project/runtime/pkg/controller"
16+
"github.com/cosi-project/runtime/pkg/safe"
17+
"go.uber.org/zap"
18+
19+
"github.com/siderolabs/talos/pkg/lvm"
20+
"github.com/siderolabs/talos/pkg/machinery/resources/block"
21+
"github.com/siderolabs/talos/pkg/machinery/resources/storage"
22+
)
23+
24+
// LVMLogicalVolumeStatusController manages LVMLogicalVolumeStatus resources.
25+
type LVMLogicalVolumeStatusController struct{}
26+
27+
// Name implements controller.Controller interface.
28+
func (ctrl *LVMLogicalVolumeStatusController) Name() string {
29+
return "storage.LVMLogicalVolumeStatusController"
30+
}
31+
32+
// Inputs implements controller.Controller interface.
33+
func (ctrl *LVMLogicalVolumeStatusController) Inputs() []controller.Input {
34+
return []controller.Input{
35+
{
36+
Namespace: block.NamespaceName,
37+
Type: block.DiscoveredVolumeType,
38+
Kind: controller.InputWeak,
39+
},
40+
{
41+
Namespace: storage.NamespaceName,
42+
Type: storage.LVMPhysicalVolumeStatusType,
43+
Kind: controller.InputWeak,
44+
},
45+
}
46+
}
47+
48+
// Outputs implements controller.Controller interface.
49+
func (ctrl *LVMLogicalVolumeStatusController) Outputs() []controller.Output {
50+
return []controller.Output{
51+
{
52+
Type: storage.LVMLogicalVolumeStatusType,
53+
Kind: controller.OutputExclusive,
54+
},
55+
}
56+
}
57+
58+
// Run implements controller.Controller interface.
59+
func (ctrl *LVMLogicalVolumeStatusController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
60+
ticker := time.NewTicker(pollInterval)
61+
defer ticker.Stop()
62+
63+
for {
64+
select {
65+
case <-ctx.Done():
66+
return nil
67+
case <-r.EventCh():
68+
case <-ticker.C:
69+
}
70+
71+
if err := ctrl.reconcile(ctx, r, logger); err != nil {
72+
return err
73+
}
74+
}
75+
}
76+
77+
func (ctrl *LVMLogicalVolumeStatusController) reconcile(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
78+
lvs, err := lvm.LVDisplay(ctx)
79+
if err != nil {
80+
if errors.Is(err, exec.ErrNotFound) {
81+
logger.Debug("lvm binary not found; skipping lv scan")
82+
83+
return nil
84+
}
85+
86+
return fmt.Errorf("lvdisplay: %w", err)
87+
}
88+
89+
r.StartTrackingOutputs()
90+
91+
for _, lv := range lvs {
92+
if err := safe.WriterModify(ctx, r, storage.NewLVMLogicalVolumeStatus(storage.NamespaceName, lvID(lv.Path)), func(s *storage.LVMLogicalVolumeStatus) error {
93+
spec := s.TypedSpec()
94+
spec.Path = lv.Path
95+
spec.VGName = lv.VGName
96+
spec.Access = lv.Access
97+
spec.Status = lv.Status
98+
spec.InternalLVNumber = lv.InternalLVNumber
99+
spec.OpenCount = lv.OpenCount
100+
spec.SizeBytes = lv.SizeSectors * sectorSize
101+
spec.CurrentLE = lv.CurrentLE
102+
spec.AllocatedLE = lv.AllocatedLE
103+
spec.AllocPolicy = lv.AllocPolicy
104+
spec.ReadAheadSectors = lv.ReadAheadSectors
105+
spec.Major = uint32(lv.Major)
106+
spec.Minor = uint32(lv.Minor)
107+
108+
return nil
109+
}); err != nil {
110+
return fmt.Errorf("modify lv %q: %w", lv.Path, err)
111+
}
112+
}
113+
114+
if err := safe.CleanupOutputs[*storage.LVMLogicalVolumeStatus](ctx, r); err != nil {
115+
return fmt.Errorf("cleanup lv outputs: %w", err)
116+
}
117+
118+
return nil
119+
}
120+
121+
// lvID derives a resource ID from an LV path. /dev/vg0/data → vg0-data.
122+
func lvID(path string) string {
123+
return strings.TrimPrefix(strings.ReplaceAll(path, "/", "-"), "-dev-")
124+
}

0 commit comments

Comments
 (0)