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") ```