mirror of
https://github.com/siderolabs/talos.git
synced 2026-05-05 04:16:21 +02:00
This is a regression compared to Talos 1.12: allow blockdevice wipe in maintenance mode (with `os:reader` role). Also improve the test for maintenance via SideroLink - add a test on install, META write and reboot preserving META value. Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
356 lines
11 KiB
Go
356 lines
11 KiB
Go
// 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 internal contains server implementation.
|
|
package internal
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"path/filepath"
|
|
"slices"
|
|
|
|
"github.com/cosi-project/runtime/pkg/safe"
|
|
"github.com/cosi-project/runtime/pkg/state"
|
|
blockdev "github.com/siderolabs/go-blockdevice/v2/block"
|
|
"github.com/siderolabs/go-blockdevice/v2/partitioning/gpt"
|
|
"golang.org/x/sys/unix"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
|
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
|
|
"github.com/siderolabs/talos/internal/pkg/partition"
|
|
"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.
|
|
//
|
|
// It is only kept here for compatibility purposes, proper API is to query `block.Disk` resources.
|
|
type Server struct {
|
|
storage.UnimplementedStorageServiceServer
|
|
|
|
Controller runtime.Controller
|
|
}
|
|
|
|
// Disks implements storage.StorageService.
|
|
func (s *Server) Disks(ctx context.Context, in *emptypb.Empty) (reply *storage.DisksResponse, err error) {
|
|
st := s.Controller.Runtime().State().V1Alpha2().Resources()
|
|
|
|
systemDisk, err := safe.StateGetByID[*block.SystemDisk](ctx, st, block.SystemDiskID)
|
|
if err != nil && !state.IsNotFoundError(err) {
|
|
return nil, err
|
|
}
|
|
|
|
disks, err := safe.StateListAll[*block.Disk](ctx, st)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
diskConv := func(d *block.Disk) *storage.Disk {
|
|
var diskType storage.Disk_DiskType
|
|
|
|
switch {
|
|
case d.TypedSpec().CDROM:
|
|
diskType = storage.Disk_CD
|
|
case d.TypedSpec().Transport == "nvme":
|
|
diskType = storage.Disk_NVME
|
|
case d.TypedSpec().Transport == "mmc":
|
|
diskType = storage.Disk_SD
|
|
case d.TypedSpec().Rotational:
|
|
diskType = storage.Disk_HDD
|
|
case d.TypedSpec().Transport != "":
|
|
diskType = storage.Disk_SSD
|
|
}
|
|
|
|
return &storage.Disk{
|
|
DeviceName: filepath.Join("/dev", d.Metadata().ID()),
|
|
Model: d.TypedSpec().Model,
|
|
Size: d.TypedSpec().Size,
|
|
Serial: d.TypedSpec().Serial,
|
|
Modalias: d.TypedSpec().Modalias,
|
|
Wwid: d.TypedSpec().WWID,
|
|
Uuid: d.TypedSpec().UUID,
|
|
Type: diskType,
|
|
BusPath: d.TypedSpec().BusPath,
|
|
SystemDisk: systemDisk != nil && d.Metadata().ID() == systemDisk.TypedSpec().DiskID,
|
|
Subsystem: d.TypedSpec().SubSystem,
|
|
Readonly: d.TypedSpec().Readonly,
|
|
}
|
|
}
|
|
|
|
reply = &storage.DisksResponse{
|
|
Messages: []*storage.Disks{
|
|
{
|
|
Disks: safe.ToSlice(disks, diskConv),
|
|
},
|
|
},
|
|
}
|
|
|
|
return reply, nil
|
|
}
|
|
|
|
// BlockDeviceWipe implements storage.StorageService.
|
|
//
|
|
// It allows to wipe unused block devices, for blockdevices in use (volumes), use a different method.
|
|
func (s *Server) BlockDeviceWipe(ctx context.Context, req *storage.BlockDeviceWipeRequest) (*storage.BlockDeviceWipeResponse, error) {
|
|
// this API is allowed for reader role only in maintenance mode.
|
|
roles := authz.GetRoles(ctx)
|
|
inMaintenance := !s.Controller.Runtime().ConfigCompleteForBoot()
|
|
|
|
if !inMaintenance && !roles.Includes(role.Admin) {
|
|
return nil, authz.ErrNotAuthorized
|
|
}
|
|
|
|
// validate the list of devices
|
|
for _, deviceRequest := range req.GetDevices() {
|
|
if err := s.validateDeviceForWipe(ctx, deviceRequest.GetDevice(), deviceRequest.GetSkipVolumeCheck(), deviceRequest.GetSkipSecondaryCheck()); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// perform the actual wipe
|
|
for _, deviceRequest := range req.GetDevices() {
|
|
if err := s.wipeDevice(deviceRequest.GetDevice(), deviceRequest.GetMethod(), deviceRequest.GetDropPartition()); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &storage.BlockDeviceWipeResponse{
|
|
Messages: []*storage.BlockDeviceWipe{
|
|
{},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
//nolint:gocyclo,cyclop
|
|
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()
|
|
|
|
blockdevice, err := safe.StateGetByID[*block.Device](ctx, st, deviceName)
|
|
if err != nil {
|
|
if state.IsNotFoundError(err) {
|
|
return status.Errorf(codes.NotFound, "blockdevice %q not found", deviceName)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
var parent string
|
|
|
|
deviceType := blockdevice.TypedSpec().Type
|
|
|
|
switch deviceType {
|
|
case block.DeviceTypeDisk: // supported
|
|
case block.DeviceTypePartition: // supported
|
|
parent = blockdevice.TypedSpec().Parent
|
|
default:
|
|
return status.Errorf(codes.InvalidArgument, "blockdevice %q is of unsupported type %q", deviceName, deviceType)
|
|
}
|
|
|
|
// check the disk (or parent)
|
|
var disk *block.Disk
|
|
|
|
if parent != "" {
|
|
disk, err = safe.StateGetByID[*block.Disk](ctx, st, parent)
|
|
} else {
|
|
disk, err = safe.StateGetByID[*block.Disk](ctx, st, deviceName)
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get disk (or parent) for %q: %w", deviceName, err)
|
|
}
|
|
|
|
if disk.TypedSpec().Readonly {
|
|
return status.Errorf(codes.FailedPrecondition, "blockdevice %q is read-only", deviceName)
|
|
}
|
|
|
|
if disk.TypedSpec().CDROM {
|
|
return status.Errorf(codes.FailedPrecondition, "blockdevice %q is a CD-ROM", deviceName)
|
|
}
|
|
|
|
// secondaries check
|
|
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())
|
|
}
|
|
}
|
|
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())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// volume in use checks
|
|
if !skipVolumeCheck {
|
|
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 filepath.Base(volumeStatus.TypedSpec().ParentLocation) == deviceName {
|
|
return status.Errorf(codes.FailedPrecondition, "blockdevice %q is in use by volume %q", deviceName, volumeStatus.Metadata().ID())
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) findParentDevice(deviceName string) (string, int, error) {
|
|
st := s.Controller.Runtime().State().V1Alpha2().Resources()
|
|
|
|
blockdevice, err := safe.StateGetByID[*block.Device](context.Background(), st, deviceName)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
|
|
if blockdevice.TypedSpec().Type == block.DeviceTypePartition {
|
|
return blockdevice.TypedSpec().Parent, blockdevice.TypedSpec().PartitionNumber, nil
|
|
}
|
|
|
|
return "", 0, nil
|
|
}
|
|
|
|
// wipeDevice wipes the block device with the given method.
|
|
//
|
|
//nolint:gocyclo,cyclop
|
|
func (s *Server) wipeDevice(deviceName string, method storage.BlockDeviceWipeDescriptor_Method, dropPartition bool) error {
|
|
parentName, partitionNumber, err := s.findParentDevice(deviceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var parentBd *blockdev.Device
|
|
|
|
if parentName != "" {
|
|
parentBd, err = blockdev.NewFromPath(filepath.Join("/dev", parentName), blockdev.OpenForWrite())
|
|
if err != nil {
|
|
return status.Errorf(codes.Internal, "failed to open block device %q: %v", parentName, err)
|
|
}
|
|
|
|
defer parentBd.Close() //nolint:errcheck
|
|
}
|
|
|
|
bd, err := blockdev.NewFromPath(filepath.Join("/dev", deviceName),
|
|
blockdev.OpenForWrite(),
|
|
blockdev.OpenAssertNotMounted(),
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, unix.EBUSY) {
|
|
return status.Errorf(codes.FailedPrecondition, "block device %q is mounted or in use", deviceName)
|
|
}
|
|
|
|
return status.Errorf(codes.Internal, "failed to open block device %q: %v", deviceName, err)
|
|
}
|
|
|
|
defer bd.Close() //nolint:errcheck
|
|
|
|
// lock the parent device always (if available)
|
|
if parentBd != nil {
|
|
log.Printf("locking parent block device %q", parentName)
|
|
|
|
if err = parentBd.Lock(true); err != nil {
|
|
return status.Errorf(codes.Internal, "failed to lock parent block device %q: %v", parentName, err)
|
|
}
|
|
|
|
defer parentBd.Unlock() //nolint:errcheck
|
|
} else {
|
|
log.Printf("locking block device %q", deviceName)
|
|
|
|
if err = bd.Lock(true); err != nil {
|
|
return status.Errorf(codes.Internal, "failed to lock block device %q: %v", deviceName, err)
|
|
}
|
|
|
|
defer bd.Unlock() //nolint:errcheck
|
|
}
|
|
|
|
switch method {
|
|
case storage.BlockDeviceWipeDescriptor_ZEROES:
|
|
log.Printf("wiping block device %q with zeroes", deviceName)
|
|
|
|
method, err := bd.Wipe()
|
|
if err != nil {
|
|
return status.Errorf(codes.Internal, "failed to wipe block device %q: %v", deviceName, err)
|
|
}
|
|
|
|
log.Printf("block device %q wiped with method %q", deviceName, method)
|
|
case storage.BlockDeviceWipeDescriptor_FAST:
|
|
log.Printf("wiping block device %q with fast method", deviceName)
|
|
|
|
if err = partition.WipeWithSignatures(bd, deviceName, log.Printf); err != nil {
|
|
return status.Error(codes.Internal, err.Error())
|
|
}
|
|
default:
|
|
return status.Errorf(codes.InvalidArgument, "unsupported wipe method %s", method)
|
|
}
|
|
|
|
if dropPartition && parentBd != nil && partitionNumber != 0 {
|
|
// first, close the blockdevice, otherwise the partition table cannot be modified
|
|
if err = bd.Close(); err != nil {
|
|
return status.Errorf(codes.Internal, "failed to close block device %q: %v", deviceName, err)
|
|
}
|
|
|
|
gptdev, err := gpt.DeviceFromBlockDevice(parentBd)
|
|
if err != nil {
|
|
return status.Errorf(codes.Internal, "failed to get GPT device: %v", err)
|
|
}
|
|
|
|
pt, err := gpt.Read(gptdev)
|
|
if err != nil {
|
|
return status.Errorf(codes.Internal, "failed to read GPT table: %v", err)
|
|
}
|
|
|
|
if err = pt.DeletePartition(partitionNumber - 1); err != nil {
|
|
return status.Errorf(codes.Internal, "failed to delete partition: %v", err)
|
|
}
|
|
|
|
if err = pt.Write(); err != nil {
|
|
return status.Errorf(codes.Internal, "failed to write GPT table: %v", err)
|
|
}
|
|
|
|
log.Printf("deleted partition %d from block device %q", partitionNumber, parentName)
|
|
}
|
|
|
|
return nil
|
|
}
|