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 <andrey.smirnov@siderolabs.com>
This commit is contained in:
Andrey Smirnov 2025-07-09 17:44:21 +04:00
parent 850579448e
commit 52656cc3c1
No known key found for this signature in database
GPG Key ID: FE042E3D4085A811
8 changed files with 138 additions and 76 deletions

View File

@ -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;
}

View File

@ -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)
}

View File

@ -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 <disk> --insecure`).
"""
[make_deps]

View File

@ -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())
}
}
}

View File

@ -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" +

View File

@ -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:])

View File

@ -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). |

View File

@ -3162,6 +3162,7 @@ talosctl wipe disk <device names>... [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")
```