From 52656cc3c11e2d326aa19cb5d70a5b2ac0ebb217 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Wed, 9 Jul 2025 17:44:21 +0400 Subject: [PATCH] feat: allow taloscl disk wipe in maintenance mode Fixes #10011 Also implement a hidden option to skip secondary disks check which allows to wipe disks which are used as part of active LVM volume. This is unsafe in general, but sometimes if you know what you're doing, it's fine. Signed-off-by: Andrey Smirnov --- api/storage/storage.proto | 2 + cmd/talosctl/cmd/talos/wipe.go | 54 +++++---- hack/release.toml | 6 + internal/app/storaged/server.go | 103 +++++++++--------- pkg/machinery/api/storage/storage.pb.go | 14 ++- .../api/storage/storage_vtproto.pb.go | 33 ++++++ website/content/v1.11/reference/api.md | 1 + website/content/v1.11/reference/cli.md | 1 + 8 files changed, 138 insertions(+), 76 deletions(-) diff --git a/api/storage/storage.proto b/api/storage/storage.proto index 187978e91..c6ca58022 100644 --- a/api/storage/storage.proto +++ b/api/storage/storage.proto @@ -93,6 +93,8 @@ message BlockDeviceWipeDescriptor { Method method = 2; // Skip the volume in use check. bool skip_volume_check = 3; + // Skip the secondary disk check (e.g. underlying disk for RAID or LVM). + bool skip_secondary_check = 5; // Drop the partition (only applies if the device is a partition). bool drop_partition = 4; } diff --git a/cmd/talosctl/cmd/talos/wipe.go b/cmd/talosctl/cmd/talos/wipe.go index 0a5e92886..8db10cdd1 100644 --- a/cmd/talosctl/cmd/talos/wipe.go +++ b/cmd/talosctl/cmd/talos/wipe.go @@ -23,9 +23,11 @@ var wipeCmd = &cobra.Command{ } var wipeDiskCmdFlags struct { - wipeMethod string - skipVolumeCheck bool - dropPartition bool + wipeMethod string + skipVolumeCheck bool + skipSecondaryCheck bool + dropPartition bool + insecure bool } // wipeDiskCmd represents the wipe disk command. @@ -37,26 +39,35 @@ var wipeDiskCmd = &cobra.Command{ Use device names as arguments, for example: vda or sda5.`, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return WithClient(func(ctx context.Context, c *client.Client) error { - method, ok := storage.BlockDeviceWipeDescriptor_Method_value[wipeDiskCmdFlags.wipeMethod] - if !ok { - return fmt.Errorf("invalid wipe method %q", wipeDiskCmdFlags.wipeMethod) - } + if wipeDiskCmdFlags.insecure { + return WithClientMaintenance(nil, cmdWipe(args)) + } - return c.BlockDeviceWipe(ctx, &storage.BlockDeviceWipeRequest{ - Devices: xslices.Map(args, func(devName string) *storage.BlockDeviceWipeDescriptor { - return &storage.BlockDeviceWipeDescriptor{ - Device: devName, - Method: storage.BlockDeviceWipeDescriptor_Method(method), - SkipVolumeCheck: wipeDiskCmdFlags.skipVolumeCheck, - DropPartition: wipeDiskCmdFlags.dropPartition, - } - }), - }) - }) + return WithClient(cmdWipe(args)) }, } +func cmdWipe(args []string) func(ctx context.Context, c *client.Client) error { + return func(ctx context.Context, c *client.Client) error { + method, ok := storage.BlockDeviceWipeDescriptor_Method_value[wipeDiskCmdFlags.wipeMethod] + if !ok { + return fmt.Errorf("invalid wipe method %q", wipeDiskCmdFlags.wipeMethod) + } + + return c.BlockDeviceWipe(ctx, &storage.BlockDeviceWipeRequest{ + Devices: xslices.Map(args, func(devName string) *storage.BlockDeviceWipeDescriptor { + return &storage.BlockDeviceWipeDescriptor{ + Device: devName, + Method: storage.BlockDeviceWipeDescriptor_Method(method), + SkipVolumeCheck: wipeDiskCmdFlags.skipVolumeCheck, + SkipSecondaryCheck: wipeDiskCmdFlags.skipSecondaryCheck, + DropPartition: wipeDiskCmdFlags.dropPartition, + } + }), + }) + } +} + func wipeMethodValues() []string { var method storage.BlockDeviceWipeDescriptor_Method @@ -74,8 +85,11 @@ func init() { wipeDiskCmd.Flags().StringVar(&wipeDiskCmdFlags.wipeMethod, "method", wipeMethodValues()[0], fmt.Sprintf("wipe method to use %s", wipeMethodValues())) wipeDiskCmd.Flags().BoolVar(&wipeDiskCmdFlags.skipVolumeCheck, "skip-volume-check", false, "skip volume check") + wipeDiskCmd.Flags().BoolVar(&wipeDiskCmdFlags.skipSecondaryCheck, "skip-secondary-check", false, "skip secondary disk check (e.g. underlying disk for RAID or LVM), use with caution") wipeDiskCmd.Flags().BoolVar(&wipeDiskCmdFlags.dropPartition, "drop-partition", false, "drop partition after wipe (if applicable)") - wipeDiskCmd.Flags().MarkHidden("skip-volume-check") //nolint:errcheck + wipeDiskCmd.Flags().MarkHidden("skip-volume-check") //nolint:errcheck + wipeDiskCmd.Flags().MarkHidden("skip-secondary-check") //nolint:errcheck + wipeDiskCmd.Flags().BoolVarP(&wipeDiskCmdFlags.insecure, "insecure", "i", false, "use Talos maintenance mode API") wipeCmd.AddCommand(wipeDiskCmd) } diff --git a/hack/release.toml b/hack/release.toml index 2d768bf90..d9916aa1f 100644 --- a/hack/release.toml +++ b/hack/release.toml @@ -95,6 +95,12 @@ Talosctl now returns the loaded modules, not the modules configured to be loaded description = """\ Talos now publishes Software Bill of Materials (SBOM) in the SPDX format. The SBOM is available in the `/usr/share/sbom` directory on the machine and can be retrieved using `talosctl get sbom`. +""" + + [notes.disk_wipe] + title = "Disk Wipe" + description = """\ +Talos now supports `talosctl disk wipe` command in maintenance mode (`talosctl disk wipe --insecure`). """ [make_deps] diff --git a/internal/app/storaged/server.go b/internal/app/storaged/server.go index 8133ca112..c2abe50d3 100644 --- a/internal/app/storaged/server.go +++ b/internal/app/storaged/server.go @@ -24,10 +24,8 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "github.com/siderolabs/talos/internal/app/machined/pkg/runtime" - "github.com/siderolabs/talos/pkg/grpc/middleware/authz" "github.com/siderolabs/talos/pkg/machinery/api/storage" "github.com/siderolabs/talos/pkg/machinery/resources/block" - "github.com/siderolabs/talos/pkg/machinery/role" ) // Server implements storage.StorageService. @@ -103,14 +101,11 @@ func (s *Server) Disks(ctx context.Context, in *emptypb.Empty) (reply *storage.D func (s *Server) BlockDeviceWipe(ctx context.Context, req *storage.BlockDeviceWipeRequest) (*storage.BlockDeviceWipeResponse, error) { // the storage server is included both into machined and maintenance service // in apid/machined mode, the normal authz checks are used before reaching this method - // in maintenance mode, do the role check, which maps today to SideroLink API connection - if s.MaintenanceMode && !authz.HasRole(ctx, role.Admin) { - return nil, status.Error(codes.Unimplemented, "API is not implemented in maintenance mode") - } - + // in maintenance mode, we allow this method to be accessible, as it only allows to wipe block devices + // // validate the list of devices for _, deviceRequest := range req.GetDevices() { - if err := s.validateDeviceForWipe(ctx, deviceRequest.GetDevice(), deviceRequest.GetSkipVolumeCheck()); err != nil { + if err := s.validateDeviceForWipe(ctx, deviceRequest.GetDevice(), deviceRequest.GetSkipVolumeCheck(), deviceRequest.GetSkipSecondaryCheck()); err != nil { return nil, err } } @@ -130,7 +125,7 @@ func (s *Server) BlockDeviceWipe(ctx context.Context, req *storage.BlockDeviceWi } //nolint:gocyclo,cyclop -func (s *Server) validateDeviceForWipe(ctx context.Context, deviceName string, skipVolumeCheck bool) error { +func (s *Server) validateDeviceForWipe(ctx context.Context, deviceName string, skipVolumeCheck, skipSecondaryCheck bool) error { // first, resolve the blockdevice and figure out what type it is st := s.Controller.Runtime().State().V1Alpha2().Resources() @@ -177,59 +172,59 @@ func (s *Server) validateDeviceForWipe(ctx context.Context, deviceName string, s } // secondaries check - switch deviceType { - case block.DeviceTypeDisk: // for disks, check secondaries even if the partition is used as secondary (track via Disk resource) - disks, err := safe.StateListAll[*block.Disk](ctx, st) - if err != nil { - return err - } + if !skipSecondaryCheck { + switch deviceType { + case block.DeviceTypeDisk: // for disks, check secondaries even if the partition is used as secondary (track via Disk resource) + disks, err := safe.StateListAll[*block.Disk](ctx, st) + if err != nil { + return err + } - for disk := range disks.All() { - if slices.Index(disk.TypedSpec().SecondaryDisks, deviceName) != -1 { - return status.Errorf(codes.FailedPrecondition, "blockdevice %q is in use by disk %q", deviceName, disk.Metadata().ID()) + for disk := range disks.All() { + if slices.Index(disk.TypedSpec().SecondaryDisks, deviceName) != -1 { + return status.Errorf(codes.FailedPrecondition, "blockdevice %q is in use by disk %q", deviceName, disk.Metadata().ID()) + } + } + case block.DeviceTypePartition: // for partitions, check secondaries only if the partition is used as a secondary + blockdevices, err := safe.StateListAll[*block.Device](ctx, st) + if err != nil { + return err + } + + for blockdevice := range blockdevices.All() { + if slices.Index(blockdevice.TypedSpec().Secondaries, deviceName) != -1 { + return status.Errorf(codes.FailedPrecondition, "blockdevice %q is in use by blockdevice %q", deviceName, blockdevice.Metadata().ID()) + } } } - case block.DeviceTypePartition: // for partitions, check secondaries only if the partition is used as a secondary - blockdevices, err := safe.StateListAll[*block.Device](ctx, st) - if err != nil { - return err - } - - for blockdevice := range blockdevices.All() { - if slices.Index(blockdevice.TypedSpec().Secondaries, deviceName) != -1 { - return status.Errorf(codes.FailedPrecondition, "blockdevice %q is in use by blockdevice %q", deviceName, blockdevice.Metadata().ID()) - } - } - } - - if skipVolumeCheck { - return nil } // volume in use checks - volumeStatuses, err := safe.StateListAll[*block.VolumeStatus](ctx, st) - if err != nil { - return err - } - - for volumeStatus := range volumeStatuses.All() { - for _, location := range []string{ - filepath.Base(volumeStatus.TypedSpec().Location), - filepath.Base(volumeStatus.TypedSpec().MountLocation), - } { - for _, dev := range []string{deviceName, parent} { - if dev == "" || location == "" { - continue - } - - if location == dev { - return status.Errorf(codes.FailedPrecondition, "blockdevice %q is in use by volume %q", dev, volumeStatus.Metadata().ID()) - } - } + if !skipVolumeCheck { + volumeStatuses, err := safe.StateListAll[*block.VolumeStatus](ctx, st) + if err != nil { + return err } - if filepath.Base(volumeStatus.TypedSpec().ParentLocation) == deviceName { - return status.Errorf(codes.FailedPrecondition, "blockdevice %q is in use by volume %q", deviceName, volumeStatus.Metadata().ID()) + for volumeStatus := range volumeStatuses.All() { + for _, location := range []string{ + filepath.Base(volumeStatus.TypedSpec().Location), + filepath.Base(volumeStatus.TypedSpec().MountLocation), + } { + for _, dev := range []string{deviceName, parent} { + if dev == "" || location == "" { + continue + } + + if location == dev { + return status.Errorf(codes.FailedPrecondition, "blockdevice %q is in use by volume %q", dev, volumeStatus.Metadata().ID()) + } + } + } + + if filepath.Base(volumeStatus.TypedSpec().ParentLocation) == deviceName { + return status.Errorf(codes.FailedPrecondition, "blockdevice %q is in use by volume %q", deviceName, volumeStatus.Metadata().ID()) + } } } diff --git a/pkg/machinery/api/storage/storage.pb.go b/pkg/machinery/api/storage/storage.pb.go index 505552239..8982f5293 100644 --- a/pkg/machinery/api/storage/storage.pb.go +++ b/pkg/machinery/api/storage/storage.pb.go @@ -441,6 +441,8 @@ type BlockDeviceWipeDescriptor struct { Method BlockDeviceWipeDescriptor_Method `protobuf:"varint,2,opt,name=method,proto3,enum=storage.BlockDeviceWipeDescriptor_Method" json:"method,omitempty"` // Skip the volume in use check. SkipVolumeCheck bool `protobuf:"varint,3,opt,name=skip_volume_check,json=skipVolumeCheck,proto3" json:"skip_volume_check,omitempty"` + // Skip the secondary disk check (e.g. underlying disk for RAID or LVM). + SkipSecondaryCheck bool `protobuf:"varint,5,opt,name=skip_secondary_check,json=skipSecondaryCheck,proto3" json:"skip_secondary_check,omitempty"` // Drop the partition (only applies if the device is a partition). DropPartition bool `protobuf:"varint,4,opt,name=drop_partition,json=dropPartition,proto3" json:"drop_partition,omitempty"` unknownFields protoimpl.UnknownFields @@ -498,6 +500,13 @@ func (x *BlockDeviceWipeDescriptor) GetSkipVolumeCheck() bool { return false } +func (x *BlockDeviceWipeDescriptor) GetSkipSecondaryCheck() bool { + if x != nil { + return x.SkipSecondaryCheck + } + return false +} + func (x *BlockDeviceWipeDescriptor) GetDropPartition() bool { if x != nil { return x.DropPartition @@ -628,11 +637,12 @@ const file_storage_storage_proto_rawDesc = "" + "\rDisksResponse\x12*\n" + "\bmessages\x18\x01 \x03(\v2\x0e.storage.DisksR\bmessages\"V\n" + "\x16BlockDeviceWipeRequest\x12<\n" + - "\adevices\x18\x01 \x03(\v2\".storage.BlockDeviceWipeDescriptorR\adevices\"\xe9\x01\n" + + "\adevices\x18\x01 \x03(\v2\".storage.BlockDeviceWipeDescriptorR\adevices\"\x9b\x02\n" + "\x19BlockDeviceWipeDescriptor\x12\x16\n" + "\x06device\x18\x01 \x01(\tR\x06device\x12A\n" + "\x06method\x18\x02 \x01(\x0e2).storage.BlockDeviceWipeDescriptor.MethodR\x06method\x12*\n" + - "\x11skip_volume_check\x18\x03 \x01(\bR\x0fskipVolumeCheck\x12%\n" + + "\x11skip_volume_check\x18\x03 \x01(\bR\x0fskipVolumeCheck\x120\n" + + "\x14skip_secondary_check\x18\x05 \x01(\bR\x12skipSecondaryCheck\x12%\n" + "\x0edrop_partition\x18\x04 \x01(\bR\rdropPartition\"\x1e\n" + "\x06Method\x12\b\n" + "\x04FAST\x10\x00\x12\n" + diff --git a/pkg/machinery/api/storage/storage_vtproto.pb.go b/pkg/machinery/api/storage/storage_vtproto.pb.go index 608faaaf2..db84fc4d4 100644 --- a/pkg/machinery/api/storage/storage_vtproto.pb.go +++ b/pkg/machinery/api/storage/storage_vtproto.pb.go @@ -335,6 +335,16 @@ func (m *BlockDeviceWipeDescriptor) MarshalToSizedBufferVT(dAtA []byte) (int, er i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if m.SkipSecondaryCheck { + i-- + if m.SkipSecondaryCheck { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x28 + } if m.DropPartition { i-- if m.DropPartition { @@ -605,6 +615,9 @@ func (m *BlockDeviceWipeDescriptor) SizeVT() (n int) { if m.DropPartition { n += 2 } + if m.SkipSecondaryCheck { + n += 2 + } n += len(m.unknownFields) return n } @@ -1481,6 +1494,26 @@ func (m *BlockDeviceWipeDescriptor) UnmarshalVT(dAtA []byte) error { } } m.DropPartition = bool(v != 0) + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field SkipSecondaryCheck", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.SkipSecondaryCheck = bool(v != 0) default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/website/content/v1.11/reference/api.md b/website/content/v1.11/reference/api.md index b93ba912a..d7111ea3f 100644 --- a/website/content/v1.11/reference/api.md +++ b/website/content/v1.11/reference/api.md @@ -9280,6 +9280,7 @@ The device should not be used as a secondary (e.g. part of LVM). The name should be submitted without `/dev/` prefix. | | method | [BlockDeviceWipeDescriptor.Method](#storage.BlockDeviceWipeDescriptor.Method) | | Wipe method to use. | | skip_volume_check | [bool](#bool) | | Skip the volume in use check. | +| skip_secondary_check | [bool](#bool) | | Skip the secondary disk check (e.g. underlying disk for RAID or LVM). | | drop_partition | [bool](#bool) | | Drop the partition (only applies if the device is a partition). | diff --git a/website/content/v1.11/reference/cli.md b/website/content/v1.11/reference/cli.md index 517d8a7f1..0814899c5 100644 --- a/website/content/v1.11/reference/cli.md +++ b/website/content/v1.11/reference/cli.md @@ -3162,6 +3162,7 @@ talosctl wipe disk ... [flags] ``` --drop-partition drop partition after wipe (if applicable) -h, --help help for disk + -i, --insecure use Talos maintenance mode API --method string wipe method to use [FAST ZEROES] (default "FAST") ```