Skip to content

Commit

Permalink
feat: support lvm auto activation
Browse files Browse the repository at this point in the history
Support lvm auto-activation as per
https://man7.org/linux/man-pages/man7/lvmautoactivation.7.html.

This changes from how Talos previously used to unconditionally tried to
activate all volume groups to based on udev events.

Fixes: #9300

Signed-off-by: Noel Georgi <git@frezbo.dev>
  • Loading branch information
frezbo committed Sep 20, 2024
1 parent 8166a58 commit d8ab498
Show file tree
Hide file tree
Showing 10 changed files with 678 additions and 393 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2024-09-12T16:43:46Z by kres 8be5fa7.
# Generated on 2024-09-20T00:40:37Z by kres 8be5fa7.

name: default
concurrency:
Expand Down Expand Up @@ -2771,7 +2771,7 @@ jobs:
- name: e2e-qemu
env:
IMAGE_REGISTRY: registry.dev.siderolabs.io
QEMU_EXTRA_DISKS: "2"
QEMU_EXTRA_DISKS: "3"
QEMU_EXTRA_DISKS_DRIVERS: ide,nvme
QEMU_EXTRA_DISKS_SIZE: "10240"
WITH_CONFIG_PATCH_WORKER: '@hack/test/patches/ephemeral-nvme.yaml'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration-extensions-cron.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2024-09-09T13:58:35Z by kres 8be5fa7.
# Generated on 2024-09-20T00:40:37Z by kres 8be5fa7.

name: integration-extensions-cron
concurrency:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/integration-qemu-cron.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
#
# Generated on 2024-09-09T13:58:35Z by kres 8be5fa7.
# Generated on 2024-09-18T10:36:36Z by kres 8be5fa7.

name: integration-qemu-cron
concurrency:
Expand Down Expand Up @@ -81,7 +81,7 @@ jobs:
- name: e2e-qemu
env:
IMAGE_REGISTRY: registry.dev.siderolabs.io
QEMU_EXTRA_DISKS: "2"
QEMU_EXTRA_DISKS: "3"
QEMU_EXTRA_DISKS_DRIVERS: ide,nvme
QEMU_EXTRA_DISKS_SIZE: "10240"
WITH_CONFIG_PATCH_WORKER: '@hack/test/patches/ephemeral-nvme.yaml'
Expand Down
2 changes: 1 addition & 1 deletion .kres.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ spec:
withSudo: true
environment:
IMAGE_REGISTRY: registry.dev.siderolabs.io
QEMU_EXTRA_DISKS: "2"
QEMU_EXTRA_DISKS: "3"
QEMU_EXTRA_DISKS_SIZE: "10240"
QEMU_EXTRA_DISKS_DRIVERS: "ide,nvme"
WITH_CONFIG_PATCH_WORKER: "@hack/test/patches/ephemeral-nvme.yaml"
Expand Down
176 changes: 176 additions & 0 deletions internal/app/machined/pkg/controllers/block/lvm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package block

import (
"context"
"fmt"
"strings"

"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"github.com/hashicorp/go-multierror"
"github.com/siderolabs/gen/optional"
"github.com/siderolabs/go-cmd/pkg/cmd"
"go.uber.org/zap"

"github.com/siderolabs/talos/pkg/machinery/constants"
"github.com/siderolabs/talos/pkg/machinery/resources/block"
runtimeres "github.com/siderolabs/talos/pkg/machinery/resources/runtime"
"github.com/siderolabs/talos/pkg/machinery/resources/v1alpha1"
)

// LVMActivationController activates LVM volumes when they are discovered by the block.DiscoveryController.
type LVMActivationController struct {
seenVolumes map[string]struct{}
activatedVGs map[string]struct{}
}

// Name implements controller.Controller interface.
func (ctrl *LVMActivationController) Name() string {
return "block.LVMActivationController"
}

// Inputs implements controller.Controller interface.
func (ctrl *LVMActivationController) Inputs() []controller.Input {
return []controller.Input{
{
Namespace: v1alpha1.NamespaceName,
Type: runtimeres.MountStatusType,
ID: optional.Some(constants.EphemeralPartitionLabel),
Kind: controller.InputWeak,
},
{
Namespace: block.NamespaceName,
Type: block.DiscoveredVolumeType,
Kind: controller.InputWeak,
},
}
}

// Outputs implements controller.Controller interface.
func (ctrl *LVMActivationController) Outputs() []controller.Output {
return nil
}

// Run implements controller.Controller interface.
//
//nolint:gocyclo
func (ctrl *LVMActivationController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
if ctrl.seenVolumes == nil {
ctrl.seenVolumes = make(map[string]struct{})
}

if ctrl.activatedVGs == nil {
ctrl.activatedVGs = make(map[string]struct{})
}

for {
select {
case <-ctx.Done():
return nil
case <-r.EventCh():
}

if _, err := safe.ReaderGetByID[*runtimeres.MountStatus](ctx, r, constants.EphemeralPartitionLabel); err != nil {
if state.IsNotFoundError(err) {
// wait for the mount status to be available
continue
}

return fmt.Errorf("failed to get mount status: %w", err)
}

discoveredVolumes, err := safe.ReaderListAll[*block.DiscoveredVolume](ctx, r)
if err != nil {
return fmt.Errorf("failed to list discovered volumes: %w", err)
}

var multiErr error

for iterator := discoveredVolumes.Iterator(); iterator.Next(); {
if _, ok := ctrl.seenVolumes[iterator.Value().Metadata().ID()]; ok {
continue
}

if iterator.Value().TypedSpec().Name != "lvm2-pv" {
ctrl.seenVolumes[iterator.Value().Metadata().ID()] = struct{}{}

continue
}

logger.Info("checking device for LVM volume activation", zap.String("device", iterator.Value().TypedSpec().DevPath))

vgName, err := ctrl.checkVGNeedsActivation(ctx, iterator.Value().TypedSpec().DevPath)
if err != nil {
multiErr = multierror.Append(multiErr, err)

continue
}

if vgName == "" {
ctrl.seenVolumes[iterator.Value().Metadata().ID()] = struct{}{}

continue
}

if _, ok := ctrl.activatedVGs[vgName]; ok {
continue
}

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

// activate the volume group
if _, err = cmd.RunContext(ctx,
"/sbin/lvm",
"vgchange",
"-aay",
"--autoactivation",
"event",
vgName,
); err != nil {
return fmt.Errorf("failed to activate LVM volume %s: %w", vgName, err)
}

ctrl.activatedVGs[vgName] = struct{}{}
}

if multiErr != nil {
return multiErr
}
}
}

// checkVGNeedsActivation checks if the device is part of a volume group and returns the volume group name
// if it needs to be activated, otherwise it returns an empty string.
func (ctrl *LVMActivationController) checkVGNeedsActivation(ctx context.Context, devicePath string) (string, error) {
// first we check if all associated volumes are available
// https://man7.org/linux/man-pages/man7/lvmautoactivation.7.html
stdOut, err := cmd.RunContext(ctx,
"/sbin/lvm",
"pvscan",
"--cache",
"--verbose",
"--listvg",
"--checkcomplete",
"--vgonline",
"--autoactivation",
"event",
"--udevoutput",
devicePath,
)
if err != nil {
return "", fmt.Errorf("failed to check if LVM volume backed by device %s needs activation: %w", devicePath, err)
}

if strings.HasPrefix(stdOut, "LVM_VG_NAME_INCOMPLETE") {
return "", nil
}

vgName := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSuffix(stdOut, "\n"), "LVM_VG_NAME_COMPLETE='"), "'")

return vgName, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error
},
&block.DiscoveryController{},
&block.DisksController{},
&block.LVMActivationController{},
&block.SystemDiskController{},
&block.UserDiskConfigController{},
&block.VolumeConfigController{},
Expand Down
Loading

0 comments on commit d8ab498

Please sign in to comment.