feat: implement blockdevice watch controller

This controller combines kobject events, and scan of `/sys/block` to
build a consistent list of available block devices, updating resources
as the blockdevice changes.

Based on these resources the next step can run probe on the blockdevices
as they change to present a consistent view of filesystems/partitions.

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
Andrey Smirnov 2024-03-04 13:23:20 +04:00
parent 06e3bc0cbd
commit 15beb14780
No known key found for this signature in database
GPG Key ID: FE042E3D4085A811
28 changed files with 3220 additions and 6 deletions

View File

@ -100,9 +100,8 @@ linters-settings:
replace-local: true
replace-allow-list:
- gopkg.in/yaml.v3
- github.com/vmware-tanzu/sonobuoy
- golang.org/x/sys
- github.com/coredns/coredns
- github.com/mdlayher/kobject
retract-allow-no-explanation: false
exclude-forbidden: true

View File

@ -0,0 +1,38 @@
syntax = "proto3";
package talos.resource.definitions.block;
option go_package = "github.com/siderolabs/talos/pkg/machinery/api/resource/definitions/block";
// DeviceSpec is the spec for devices status.
message DeviceSpec {
string type = 1;
int64 major = 2;
int64 minor = 3;
string partition_name = 4;
int64 partition_number = 5;
int64 generation = 6;
string device_path = 7;
string parent = 8;
}
// DiscoveredVolumeSpec is the spec for DiscoveredVolumes status.
message DiscoveredVolumeSpec {
uint64 size = 1;
uint64 sector_size = 2;
uint64 io_size = 3;
string name = 4;
string uuid = 5;
string label = 6;
uint32 block_size = 7;
uint32 filesystem_block_size = 8;
uint64 probed_size = 9;
string partition_uuid = 10;
string partition_type = 11;
string partition_label = 12;
uint64 partition_index = 13;
string type = 14;
string device_path = 15;
string parent = 16;
}

6
go.mod
View File

@ -5,6 +5,10 @@ go 1.22.1
replace (
// forked coredns so we don't carry caddy and other stuff into the Talos
github.com/coredns/coredns => github.com/siderolabs/coredns v1.11.52
// see https://github.com/mdlayher/kobject/pull/5
github.com/mdlayher/kobject => github.com/smira/kobject v0.0.0-20240304111826-49c8d4613389
// Use nested module.
github.com/siderolabs/talos/pkg/machinery => ./pkg/machinery
@ -87,6 +91,7 @@ require (
github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875
github.com/mdlayher/ethtool v0.1.0
github.com/mdlayher/genetlink v1.3.2
github.com/mdlayher/kobject v0.0.0-20200520190114-19ca17470d7d
github.com/mdlayher/netlink v1.7.2
github.com/mdlayher/netx v0.0.0-20230430222610-7e21880baee8
github.com/mdp/qrterminal/v3 v3.2.0
@ -112,6 +117,7 @@ require (
github.com/siderolabs/gen v0.4.8
github.com/siderolabs/go-api-signature v0.3.2
github.com/siderolabs/go-blockdevice v0.4.7
github.com/siderolabs/go-blockdevice/v2 v2.0.0-20240301135834-a5481f5272f2
github.com/siderolabs/go-circular v0.1.0
github.com/siderolabs/go-cmd v0.1.1
github.com/siderolabs/go-copy v0.1.0

14
go.sum
View File

@ -446,6 +446,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v1.4.1 h1:JfD4jthWBqZMEffc5RjgmlzpYttAVw1sdnmiNaPO3hE=
github.com/jsimonetti/rtnetlink v1.4.1/go.mod h1:xJjT7t59UIZ62GLZbv6PLLo8VFrostJMPBAheR6OM8w=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -501,6 +503,9 @@ github.com/mdlayher/ethtool v0.1.0 h1:XAWHsmKhyPOo42qq/yTPb0eFBGUKKTR1rE0dVrWVQ0
github.com/mdlayher/ethtool v0.1.0/go.mod h1:fBMLn2UhfRGtcH5ZFjr+6GUiHEjZsItFD7fSn7jbZVQ=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/netx v0.0.0-20230430222610-7e21880baee8 h1:HMgSn3c16SXca3M+n6fLK2hXJLd4mhKAsZZh7lQfYmQ=
@ -659,6 +664,8 @@ github.com/siderolabs/go-api-signature v0.3.2 h1:blqrZF1GM7TWgq7mY7CsR+yQ93u6az0
github.com/siderolabs/go-api-signature v0.3.2/go.mod h1:punhUOaXa7LELYBRCUhfgUGH6ieVz68GrP98apCKXj8=
github.com/siderolabs/go-blockdevice v0.4.7 h1:2bk4WpEEflGxjrNwp57ye24Pr+cYgAiAeNMWiQOuWbQ=
github.com/siderolabs/go-blockdevice v0.4.7/go.mod h1:4PeOuk71pReJj1JQEXDE7kIIQJPVe8a+HZQa+qjxSEA=
github.com/siderolabs/go-blockdevice/v2 v2.0.0-20240301135834-a5481f5272f2 h1:+/vwLuWQuBL85p0XO9Ak22rqvBPreC2R+pta4Bw1HFI=
github.com/siderolabs/go-blockdevice/v2 v2.0.0-20240301135834-a5481f5272f2/go.mod h1:UBbbc+L7hU0UggOQeKCA+Qp3ImGkSeaLfVOiCbxRxEI=
github.com/siderolabs/go-circular v0.1.0 h1:zpBJNUbCZSh0odZxA4Dcj0d3ShLLR2WxKW6hTdAtoiE=
github.com/siderolabs/go-circular v0.1.0/go.mod h1:14XnLf/I3J0VjzTgmwWNGjp58/bdIi4zXppAEx8plfw=
github.com/siderolabs/go-cmd v0.1.1 h1:nTouZUSxLeiiEe7hFexSVvaTsY/3O8k1s08BxPRrsps=
@ -702,6 +709,8 @@ github.com/siderolabs/tcpproxy v0.1.0/go.mod h1:onn6CPPj/w1UNqQ0U97oRPF0CqbrgEAp
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smira/kobject v0.0.0-20240304111826-49c8d4613389 h1:f/5NRv5IGZxbjBhc5MnlbNmyuXBPxvekhBAUzyKWyLY=
github.com/smira/kobject v0.0.0-20240304111826-49c8d4613389/go.mod h1:+SexPO1ZvdbbWUdUnyXEWv3+4NwHZjKhxOmQqHY4Pqc=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
@ -886,6 +895,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -946,15 +957,18 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -0,0 +1,6 @@
// 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 provides the controllers related to blockdevices, mounts, etc.
package block

View File

@ -0,0 +1,246 @@
// 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"
"os"
"path/filepath"
"strconv"
"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"go.uber.org/zap"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/block/internal/inotify"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/block/internal/kobject"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/block/internal/sysblock"
machineruntime "github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
"github.com/siderolabs/talos/pkg/machinery/resources/block"
)
// DevicesController provides a view of available block devices with information about pending updates.
type DevicesController struct {
V1Alpha1Mode machineruntime.Mode
}
// Name implements controller.Controller interface.
func (ctrl *DevicesController) Name() string {
return "block.DevicesController"
}
// Inputs implements controller.Controller interface.
func (ctrl *DevicesController) Inputs() []controller.Input {
return nil
}
// Outputs implements controller.Controller interface.
func (ctrl *DevicesController) Outputs() []controller.Output {
return []controller.Output{
{
Type: block.DeviceType,
Kind: controller.OutputExclusive,
},
}
}
// Run implements controller.Controller interface.
//
//nolint:gocyclo
func (ctrl *DevicesController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
// in container mode, no devices
if ctrl.V1Alpha1Mode == machineruntime.ModeContainer {
return nil
}
// start the watcher first
watcher, err := kobject.NewWatcher()
if err != nil {
return fmt.Errorf("failed to create kobject watcher: %w", err)
}
defer watcher.Close() //nolint:errcheck
watchCh := watcher.Run(logger)
// start the inotify watcher
inotifyWatcher, err := inotify.NewWatcher()
if err != nil {
return fmt.Errorf("failed to create inotify watcher: %w", err)
}
defer inotifyWatcher.Close() //nolint:errcheck
inotifyCh, inotifyErrCh := inotifyWatcher.Run()
// reconcile the initial list of devices while the watcher is running
select {
case <-ctx.Done():
return nil
case <-r.EventCh():
}
if err = ctrl.resync(ctx, r, logger, inotifyWatcher); err != nil {
return fmt.Errorf("failed to resync: %w", err)
}
for {
select {
case ev := <-watchCh:
if ev.Subsystem != "block" {
continue
}
ev.DevicePath = filepath.Join("/sys", ev.DevicePath)
if err = ctrl.processEvent(ctx, r, logger, inotifyWatcher, ev); err != nil {
return err
}
case err = <-inotifyErrCh:
return fmt.Errorf("inotify watcher failed: %w", err)
case updatedPath := <-inotifyCh:
id := filepath.Base(updatedPath)
if err = ctrl.bumpGeneration(ctx, r, logger, id); err != nil {
return err
}
case <-ctx.Done():
return nil
}
}
}
func (ctrl *DevicesController) bumpGeneration(ctx context.Context, r controller.Runtime, logger *zap.Logger, id string) error {
_, err := safe.ReaderGetByID[*block.Device](ctx, r, id)
if err != nil {
if state.IsNotFoundError(err) {
// skip it
return nil
}
return err
}
logger.Debug("bumping generation for device, inotify update", zap.String("id", id))
return safe.WriterModify(ctx, r, block.NewDevice(block.NamespaceName, id), func(dev *block.Device) error {
dev.TypedSpec().Generation++
return nil
})
}
func (ctrl *DevicesController) resync(ctx context.Context, r controller.Runtime, logger *zap.Logger, inotifyWatcher *inotify.Watcher) error {
events, err := sysblock.Walk("/sys/block")
if err != nil {
return fmt.Errorf("failed to walk /sys/block: %w", err)
}
touchedIDs := make(map[string]struct{}, len(events))
for _, ev := range events {
if err = ctrl.processEvent(ctx, r, logger, inotifyWatcher, ev); err != nil {
return err
}
touchedIDs[ev.Values["DEVNAME"]] = struct{}{}
}
// remove devices that were not touched
devices, err := safe.ReaderListAll[*block.Device](ctx, r)
if err != nil {
return fmt.Errorf("failed to list devices: %w", err)
}
for iterator := devices.Iterator(); iterator.Next(); {
dev := iterator.Value()
if _, ok := touchedIDs[dev.Metadata().ID()]; ok {
continue
}
if err = r.Destroy(ctx, dev.Metadata()); err != nil && !state.IsNotFoundError(err) {
return fmt.Errorf("failed to remove device: %w", err)
}
}
return nil
}
//nolint:gocyclo
func (ctrl *DevicesController) processEvent(ctx context.Context, r controller.Runtime, logger *zap.Logger, inotifyWatcher *inotify.Watcher, ev *kobject.Event) error {
logger = logger.With(
zap.String("action", string(ev.Action)),
zap.String("path", ev.DevicePath),
zap.String("id", ev.Values["DEVNAME"]),
)
logger.Debug("processing event")
id := ev.Values["DEVNAME"]
devPath := filepath.Join("/dev", id)
// re-stat the sysfs entry to make sure we are not out of sync with events
_, reStatErr := os.Stat(ev.DevicePath)
switch ev.Action {
case kobject.ActionAdd, kobject.ActionBind, kobject.ActionOnline, kobject.ActionChange, kobject.ActionMove, kobject.ActionUnbind, kobject.ActionOffline:
if reStatErr != nil {
logger.Debug("skipped, as device path doesn't exist")
return nil //nolint:nilerr // entry doesn't exist now, so skip the event
}
if err := safe.WriterModify(ctx, r, block.NewDevice(block.NamespaceName, id), func(dev *block.Device) error {
dev.TypedSpec().Type = ev.Values["DEVTYPE"]
dev.TypedSpec().Major = atoiOrZero(ev.Values["MAJOR"])
dev.TypedSpec().Minor = atoiOrZero(ev.Values["MINOR"])
dev.TypedSpec().PartitionName = ev.Values["PARTNAME"]
dev.TypedSpec().PartitionNumber = atoiOrZero(ev.Values["PARTN"])
dev.TypedSpec().DevicePath = ev.DevicePath
if dev.TypedSpec().Type == "partition" {
dev.TypedSpec().Parent = filepath.Base(filepath.Dir(dev.TypedSpec().DevicePath))
}
dev.TypedSpec().Generation++
return nil
}); err != nil {
return fmt.Errorf("failed to modify device %q: %w", id, err)
}
if err := inotifyWatcher.Add(devPath); err != nil {
return fmt.Errorf("failed to add inotify watch for %q: %w", devPath, err)
}
case kobject.ActionRemove:
if reStatErr == nil { // entry still exists, skip removing
logger.Debug("skipped, as device path still exists")
return nil
}
if err := r.Destroy(ctx, block.NewDevice(block.NamespaceName, id).Metadata()); err != nil && !state.IsNotFoundError(err) {
return fmt.Errorf("failed to remove device %q: %w", id, err)
}
if err := inotifyWatcher.Remove(devPath); err != nil {
return fmt.Errorf("failed to remove inotify watch for %q: %w", devPath, err)
}
default:
logger.Debug("skipped, as action is not supported")
}
return nil
}
func atoiOrZero(s string) int {
i, _ := strconv.Atoi(s) //nolint:errcheck
return i
}

View File

@ -0,0 +1,34 @@
// 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_test
import (
"testing"
"github.com/cosi-project/runtime/pkg/resource/rtestutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
blockctrls "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/block"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest"
"github.com/siderolabs/talos/pkg/machinery/resources/block"
)
type DevicesSuite struct {
ctest.DefaultSuite
}
func TestDevicesSuite(t *testing.T) {
suite.Run(t, new(DevicesSuite))
}
func (suite *DevicesSuite) TestDiscover() {
suite.Require().NoError(suite.Runtime().RegisterController(&blockctrls.DevicesController{}))
// these devices should always exist on Linux
rtestutils.AssertResources(suite.Ctx(), suite.T(), suite.State(), []string{"loop0", "loop1"}, func(r *block.Device, assertions *assert.Assertions) {
assertions.Equal("disk", r.TypedSpec().Type)
})
}

View File

@ -0,0 +1,277 @@
// 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"
"path/filepath"
"strconv"
"time"
"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"github.com/siderolabs/gen/maps"
"github.com/siderolabs/go-blockdevice/v2/blkid"
"go.uber.org/zap"
"github.com/siderolabs/talos/pkg/machinery/resources/block"
)
// DiscoveryController provides a filesystem/partition discovery for blockdevices.
type DiscoveryController struct{}
// Name implements controller.Controller interface.
func (ctrl *DiscoveryController) Name() string {
return "block.DiscoveryController"
}
// Inputs implements controller.Controller interface.
func (ctrl *DiscoveryController) Inputs() []controller.Input {
return []controller.Input{
{
Namespace: block.NamespaceName,
Type: block.DeviceType,
Kind: controller.InputWeak,
},
}
}
// Outputs implements controller.Controller interface.
func (ctrl *DiscoveryController) Outputs() []controller.Output {
return []controller.Output{
{
Type: block.DiscoveredVolumeType,
Kind: controller.OutputExclusive,
},
}
}
// Run implements controller.Controller interface.
//
//nolint:gocyclo
func (ctrl *DiscoveryController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
// lastObservedGenerations holds the last observed generation of each device.
//
// when the generation of a device changes, the device might have changed and might need to be re-probed.
lastObservedGenerations := map[string]int{}
// nextRescan holds the pool of devices to be rescanned in the next batch.
nextRescan := map[string]int{}
rescanTicker := time.NewTicker(100 * time.Millisecond)
defer rescanTicker.Stop()
for {
select {
case <-ctx.Done():
return nil
case <-rescanTicker.C:
if len(nextRescan) == 0 {
continue
}
if err := ctrl.rescan(ctx, r, logger, maps.Keys(nextRescan)); err != nil {
return fmt.Errorf("failed to rescan devices: %w", err)
}
nextRescan = map[string]int{}
case <-r.EventCh():
devices, err := safe.ReaderListAll[*block.Device](ctx, r)
if err != nil {
return fmt.Errorf("failed to list devices: %w", err)
}
parents := map[string]string{}
allDevices := map[string]struct{}{}
for iterator := devices.Iterator(); iterator.Next(); {
device := iterator.Value()
allDevices[device.Metadata().ID()] = struct{}{}
if device.TypedSpec().Parent != "" {
parents[device.Metadata().ID()] = device.TypedSpec().Parent
}
if device.TypedSpec().Generation == lastObservedGenerations[device.Metadata().ID()] {
continue
}
nextRescan[device.Metadata().ID()] = device.TypedSpec().Generation
lastObservedGenerations[device.Metadata().ID()] = device.TypedSpec().Generation
}
// remove child devices if the parent is marked for rescan
for id := range nextRescan {
if parent, ok := parents[id]; ok {
if _, ok := nextRescan[parent]; ok {
delete(nextRescan, id)
}
}
}
// if the device is removed, add it to the nextRescan, and remove from lastObservedGenerations
for id := range lastObservedGenerations {
if _, ok := allDevices[id]; !ok {
nextRescan[id] = lastObservedGenerations[id]
delete(lastObservedGenerations, id)
}
}
}
}
}
//nolint:gocyclo
func (ctrl *DiscoveryController) rescan(ctx context.Context, r controller.Runtime, logger *zap.Logger, ids []string) error {
failedIDs := map[string]struct{}{}
touchedIDs := map[string]struct{}{}
for _, id := range ids {
device, err := safe.ReaderGetByID[*block.Device](ctx, r, id)
if err != nil {
if state.IsNotFoundError(err) {
failedIDs[id] = struct{}{}
continue
}
return fmt.Errorf("failed to get device: %w", err)
}
info, err := blkid.ProbePath(filepath.Join("/dev", id))
if err != nil {
logger.Debug("failed to probe device", zap.String("id", id), zap.Error(err))
failedIDs[id] = struct{}{}
continue
}
if err = safe.WriterModify(ctx, r, block.NewDiscoveredVolume(block.NamespaceName, id), func(dv *block.DiscoveredVolume) error {
dv.TypedSpec().Type = device.TypedSpec().Type
dv.TypedSpec().DevicePath = device.TypedSpec().DevicePath
dv.TypedSpec().Parent = device.TypedSpec().Parent
dv.TypedSpec().Size = info.Size
dv.TypedSpec().SectorSize = info.SectorSize
dv.TypedSpec().IOSize = info.IOSize
ctrl.fillDiscoveredVolumeFromInfo(dv, info.ProbeResult)
return nil
}); err != nil {
return fmt.Errorf("failed to write discovered volume: %w", err)
}
touchedIDs[id] = struct{}{}
for _, nested := range info.Parts {
partID := partitionID(id, nested.PartitionIndex)
if err = safe.WriterModify(ctx, r, block.NewDiscoveredVolume(block.NamespaceName, partID), func(dv *block.DiscoveredVolume) error {
dv.TypedSpec().Type = "partition"
dv.TypedSpec().DevicePath = filepath.Join(device.TypedSpec().DevicePath, partID)
dv.TypedSpec().Parent = id
dv.TypedSpec().Size = nested.ProbedSize
dv.TypedSpec().SectorSize = info.SectorSize
dv.TypedSpec().IOSize = info.IOSize
ctrl.fillDiscoveredVolumeFromInfo(dv, nested.ProbeResult)
if nested.PartitionUUID != nil {
dv.TypedSpec().PartitionUUID = nested.PartitionUUID.String()
} else {
dv.TypedSpec().PartitionUUID = ""
}
if nested.PartitionType != nil {
dv.TypedSpec().PartitionType = nested.PartitionType.String()
} else {
dv.TypedSpec().PartitionType = ""
}
if nested.PartitionLabel != nil {
dv.TypedSpec().PartitionLabel = *nested.PartitionLabel
} else {
dv.TypedSpec().PartitionLabel = ""
}
dv.TypedSpec().PartitionIndex = nested.PartitionIndex
return nil
}); err != nil {
return fmt.Errorf("failed to write discovered volume: %w", err)
}
touchedIDs[partID] = struct{}{}
}
}
// clean up discovered volumes
discoveredVolumes, err := safe.ReaderListAll[*block.DiscoveredVolume](ctx, r)
if err != nil {
return fmt.Errorf("failed to list discovered volumes: %w", err)
}
for iterator := discoveredVolumes.Iterator(); iterator.Next(); {
dv := iterator.Value()
if _, ok := touchedIDs[dv.Metadata().ID()]; ok {
continue
}
_, isFailed := failedIDs[dv.Metadata().ID()]
parentTouched := false
if dv.TypedSpec().Parent != "" {
if _, ok := touchedIDs[dv.TypedSpec().Parent]; ok {
parentTouched = true
}
}
if isFailed || parentTouched {
// if the probe failed, or if the parent was touched, while this device was not, remove it
if err = r.Destroy(ctx, dv.Metadata()); err != nil {
return fmt.Errorf("failed to destroy discovered volume: %w", err)
}
}
}
return nil
}
func (ctrl *DiscoveryController) fillDiscoveredVolumeFromInfo(dv *block.DiscoveredVolume, info blkid.ProbeResult) {
dv.TypedSpec().Name = info.Name
if info.UUID != nil {
dv.TypedSpec().UUID = info.UUID.String()
} else {
dv.TypedSpec().UUID = ""
}
if info.Label != nil {
dv.TypedSpec().Label = *info.Label
} else {
dv.TypedSpec().Label = ""
}
dv.TypedSpec().BlockSize = info.BlockSize
dv.TypedSpec().FilesystemBlockSize = info.FilesystemBlockSize
dv.TypedSpec().ProbedSize = info.ProbedSize
}
func partitionID(devname string, part uint) string {
result := devname
if len(result) > 0 && result[len(result)-1] >= '0' && result[len(result)-1] <= '9' {
result += "p"
}
return result + strconv.FormatUint(uint64(part), 10)
}

View File

@ -0,0 +1,275 @@
// 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 inotify implements a specialized inotify watcher for block devices.
package inotify
import (
"errors"
"os"
"strings"
"sync"
"unsafe"
"golang.org/x/sys/unix"
)
type (
watches struct {
mu sync.RWMutex
wd map[uint32]*watch // wd → watch
path map[string]uint32 // pathname → wd
}
watch struct {
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
path string // Watch path.
}
)
func newWatches() *watches {
return &watches{
wd: make(map[uint32]*watch),
path: make(map[string]uint32),
}
}
func (w *watches) remove(wd uint32) {
w.mu.Lock()
defer w.mu.Unlock()
delete(w.path, w.wd[wd].path)
delete(w.wd, wd)
}
func (w *watches) removePath(path string) (uint32, bool) {
w.mu.Lock()
defer w.mu.Unlock()
wd, ok := w.path[path]
if !ok {
return 0, false
}
delete(w.path, path)
delete(w.wd, wd)
return wd, true
}
func (w *watches) byWd(wd uint32) *watch {
w.mu.RLock()
defer w.mu.RUnlock()
return w.wd[wd]
}
func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error {
w.mu.Lock()
defer w.mu.Unlock()
var existing *watch
wd, ok := w.path[path]
if ok {
existing = w.wd[wd]
}
upd, err := f(existing)
if err != nil {
return err
}
if upd != nil {
w.wd[upd.wd] = upd
w.path[upd.path] = upd.wd
if upd.wd != wd {
delete(w.wd, wd)
}
}
return nil
}
// Watcher implements inotify-based file watching.
type Watcher struct {
wg sync.WaitGroup
fd int
inotifyFile *os.File
watches *watches
}
// NewWatcher creates a new inotify Watcher.
func NewWatcher() (*Watcher, error) {
// Need to set nonblocking mode for SetDeadline to work, otherwise blocking
// I/O operations won't terminate on close.
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
if fd == -1 {
return nil, errno
}
return &Watcher{
fd: fd,
inotifyFile: os.NewFile(uintptr(fd), ""),
watches: newWatches(),
}, nil
}
// Close the inotify watcher.
func (w *Watcher) Close() error {
// Causes any blocking reads to return with an error, provided the file
// still supports deadline operations.
err := w.inotifyFile.Close()
if err != nil {
return err
}
// Wait for goroutine to close
w.wg.Wait()
return nil
}
// Run the watcher, returns two channels for errors and events (paths changed).
//
//nolint:gocyclo
func (w *Watcher) Run() (<-chan string, <-chan error) {
errCh := make(chan error, 1)
eventCh := make(chan string, 128)
w.wg.Add(1)
var buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
go func() {
defer w.wg.Done()
for {
n, err := w.inotifyFile.Read(buf[:])
switch {
case errors.Is(err, os.ErrClosed):
return
case err != nil:
errCh <- err
return
}
if n < unix.SizeofInotifyEvent {
errCh <- errors.New("short read from inotify")
return
}
var offset uint32
// We don't know how many events we just read into the buffer
// While the offset points to at least one whole event...
for offset <= uint32(n-unix.SizeofInotifyEvent) {
var (
// Point "raw" to the event in the buffer
raw = (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
mask = raw.Mask
nameLen = raw.Len
)
if mask&unix.IN_Q_OVERFLOW != 0 {
errCh <- errors.New("inotify queue overflow")
return
}
// If the event happened to the watched directory or the watched file, the kernel
// doesn't append the filename to the event, but we would like to always fill the
// the "Name" field with a valid filename. We retrieve the path of the watch from
// the "paths" map.
watch := w.watches.byWd(uint32(raw.Wd))
// inotify will automatically remove the watch on deletes; just need
// to clean our state here.
if watch != nil && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
w.watches.remove(watch.wd)
}
var name string
if watch != nil {
name = watch.path
}
if nameLen > 0 {
// Point "bytes" at the first byte of the filename
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
// The filename is padded with NULL bytes. TrimRight() gets rid of those.
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000")
}
// Send the events that are not ignored on the events channel
if mask&unix.IN_IGNORED == 0 {
eventCh <- name
}
// Move to the next event in the buffer
offset += unix.SizeofInotifyEvent + nameLen
}
}
}()
return eventCh, errCh
}
// Add a watch to the inotify watcher.
func (w *Watcher) Add(name string) error {
var flags uint32 = unix.IN_CLOSE_WRITE | unix.IN_DELETE_SELF
return w.watches.updatePath(name, func(existing *watch) (*watch, error) {
if existing != nil {
flags |= existing.flags | unix.IN_MASK_ADD
}
wd, err := unix.InotifyAddWatch(w.fd, name, flags)
if wd == -1 {
return nil, err
}
if existing == nil {
return &watch{
wd: uint32(wd),
path: name,
flags: flags,
}, nil
}
existing.wd = uint32(wd)
existing.flags = flags
return existing, nil
})
}
// Remove a watch from the inotify watcher.
func (w *Watcher) Remove(name string) error {
wd, ok := w.watches.removePath(name)
if !ok {
return nil
}
success, errno := unix.InotifyRmWatch(w.fd, wd)
if success == -1 {
// TODO: Perhaps it's not helpful to return an error here in every case;
// The only two possible errors are:
//
// - EBADF, which happens when w.fd is not a valid file descriptor
// of any kind.
// - EINVAL, which is when fd is not an inotify descriptor or wd
// is not a valid watch descriptor. Watch descriptors are
// invalidated when they are removed explicitly or implicitly;
// explicitly by inotify_rm_watch, implicitly when the file they
// are watching is deleted.
return errno
}
return nil
}

View File

@ -0,0 +1,73 @@
// 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 inotify_test
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/block/internal/inotify"
)
func TestWatcher(t *testing.T) {
watcher, err := inotify.NewWatcher()
require.NoError(t, err)
d := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(d, "file1"), []byte("test1"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(d, "file2"), []byte("test2"), 0o644))
require.NoError(t, watcher.Add(filepath.Join(d, "file1")))
watchCh, errCh := watcher.Run()
require.NoError(t, watcher.Add(filepath.Join(d, "file2")))
select {
case path := <-watchCh:
require.FailNow(t, "unexpected path", "%s", path)
case err = <-errCh:
require.FailNow(t, "unexpected error", "%s", err)
case <-time.After(100 * time.Millisecond):
}
// open file1 for writing, should get inotify event
f1, err := os.OpenFile(filepath.Join(d, "file1"), os.O_WRONLY, 0)
require.NoError(t, err)
require.NoError(t, f1.Close())
select {
case path := <-watchCh:
require.Equal(t, filepath.Join(d, "file1"), path)
case err = <-errCh:
require.FailNow(t, "unexpected error", "%s", err)
case <-time.After(time.Second):
require.FailNow(t, "timeout")
}
// open file2 for reading, should not get inotify event
f2, err := os.OpenFile(filepath.Join(d, "file2"), os.O_RDONLY, 0)
require.NoError(t, err)
require.NoError(t, f2.Close())
select {
case path := <-watchCh:
require.FailNow(t, "unexpected path", "%s", path)
case err = <-errCh:
require.FailNow(t, "unexpected error", "%s", err)
case <-time.After(100 * time.Millisecond):
}
require.NoError(t, watcher.Remove(filepath.Join(d, "file2")))
require.NoError(t, watcher.Close())
}

View File

@ -0,0 +1,90 @@
// 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 kobject implements Linux kernel kobject uvent watcher.
package kobject
import (
"fmt"
"sync"
"github.com/mdlayher/kobject"
"go.uber.org/zap"
)
const readBufferSize = 64 * 1024 * 1024
// Event is exported.
type Event = kobject.Event
// Re-export action constants.
const (
ActionAdd = kobject.Add
ActionRemove = kobject.Remove
ActionChange = kobject.Change
ActionMove = kobject.Move
ActionOnline = kobject.Online
ActionOffline = kobject.Offline
ActionBind = kobject.Bind
ActionUnbind = kobject.Unbind
)
// Watcher is a kobject uvent watcher.
type Watcher struct {
wg sync.WaitGroup
cli *kobject.Client
}
// NewWatcher creates a new kobject watcher.
func NewWatcher() (*Watcher, error) {
cli, err := kobject.New()
if err != nil {
return nil, fmt.Errorf("failed to create kobject client: %w", err)
}
if err = cli.SetReadBuffer(readBufferSize); err != nil {
return nil, err
}
return &Watcher{
cli: cli,
}, nil
}
// Close the watcher.
func (w *Watcher) Close() error {
if err := w.cli.Close(); err != nil {
return err
}
w.wg.Wait()
return nil
}
// Run the watcher, returns the channel of events.
func (w *Watcher) Run(logger *zap.Logger) <-chan *Event {
ch := make(chan *kobject.Event, 128)
w.wg.Add(1)
go func() {
defer w.wg.Done()
defer close(ch)
for {
ev, err := w.cli.Receive()
if err != nil {
logger.Error("failed to receive kobject event", zap.Error(err))
return
}
ch <- ev
}
}()
return ch
}

View File

@ -0,0 +1,27 @@
// 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 kobject_test
import (
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/block/internal/kobject"
)
func TestWatcher(t *testing.T) {
watcher, err := kobject.NewWatcher()
require.NoError(t, err)
evCh := watcher.Run(zaptest.NewLogger(t))
require.NoError(t, watcher.Close())
// the evCh should be closed
for range evCh { //nolint:revive
}
}

View File

@ -0,0 +1,144 @@
// 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 sysblock implements gathering block device information from /sys/block filesystem.
package sysblock
import (
"bytes"
"fmt"
"os"
"path/filepath"
"github.com/mdlayher/kobject"
)
// Walk the /sys/block filesystem and gather block device information.
//
//nolint:gocyclo
func Walk(root string) ([]*kobject.Event, error) {
entries, err := os.ReadDir(root)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %w", root, err)
}
result := make([]*kobject.Event, 0, len(entries))
for _, entry := range entries {
fi, err := entry.Info()
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, fmt.Errorf("failed to stat %s: %w", entry.Name(), err)
}
if fi.Mode()&os.ModeSymlink == 0 {
continue
}
path, err := filepath.EvalSymlinks(filepath.Join(root, entry.Name()))
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, fmt.Errorf("failed to resolve symlink %s: %w", entry.Name(), err)
}
uevent, err := readUevent(path)
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
result = append(result, &kobject.Event{
Action: kobject.Add,
DevicePath: path,
Subsystem: "block",
Values: uevent,
})
partitions, err := readPartitions(path)
if err != nil {
return nil, err
}
result = append(result, partitions...)
}
return result, nil
}
// readUevent reads the /sys/block/<device>/uevent file and returns the content.
func readUevent(path string) (map[string]string, error) {
path = filepath.Join(path, "uevent")
content, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %w", path, err)
}
result := map[string]string{}
for _, kv := range bytes.Split(content, []byte("\n")) {
key, value, ok := bytes.Cut(kv, []byte("="))
if !ok {
continue
}
result[string(key)] = string(value)
}
return result, nil
}
// readPartitions reads partitions for a given device and returns the list of events.
func readPartitions(path string) ([]*kobject.Event, error) {
entries, err := os.ReadDir(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to read %s: %w", path, err)
}
var result []*kobject.Event //nolint:prealloc
for _, entry := range entries {
if !entry.IsDir() {
continue
}
partitionPath := filepath.Join(path, entry.Name())
_, err = os.Stat(filepath.Join(partitionPath, "partition"))
if err != nil {
continue
}
uevent, err := readUevent(partitionPath)
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
result = append(result, &kobject.Event{
Action: kobject.Add,
DevicePath: partitionPath,
Subsystem: "block",
Values: uevent,
})
}
return result, nil
}

View File

@ -0,0 +1,42 @@
// 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 sysblock_test
import (
"testing"
"github.com/mdlayher/kobject"
"github.com/stretchr/testify/require"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/block/internal/sysblock"
)
func TestWalk(t *testing.T) {
events, err := sysblock.Walk("/sys/block")
require.NoError(t, err)
require.NotEmpty(t, events)
// there should be at least a single blockdevice and a partition
partitions, disks := 0, 0
for _, event := range events {
require.Equal(t, "block", event.Subsystem)
require.EqualValues(t, kobject.Add, event.Action)
require.NotEmpty(t, event.DevicePath)
require.NotEmpty(t, event.Action)
switch event.Values["DEVTYPE"] {
case "partition":
partitions++
case "disk":
disks++
}
}
require.Greater(t, partitions, 0)
require.Greater(t, disks, 0)
}

View File

@ -20,6 +20,7 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/block"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/cluster"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/config"
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/cri"
@ -87,6 +88,10 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error
}
for _, c := range []controller.Controller{
&block.DevicesController{
V1Alpha1Mode: ctrl.v1alpha1Runtime.State().Platform().Mode(),
},
&block.DiscoveryController{},
&cluster.AffiliateMergeController{},
cluster.NewConfigController(),
&cluster.DiscoveryServiceController{},

View File

@ -14,6 +14,7 @@ import (
"github.com/cosi-project/runtime/pkg/state/registry"
talosconfig "github.com/siderolabs/talos/pkg/machinery/config"
"github.com/siderolabs/talos/pkg/machinery/resources/block"
"github.com/siderolabs/talos/pkg/machinery/resources/cluster"
"github.com/siderolabs/talos/pkg/machinery/resources/config"
"github.com/siderolabs/talos/pkg/machinery/resources/cri"
@ -94,6 +95,8 @@ func NewState() (*State, error) {
// register Talos resources
for _, r := range []meta.ResourceWithRD{
&block.Device{},
&block.DiscoveredVolume{},
&cluster.Affiliate{},
&cluster.Config{},
&cluster.Identity{},

View File

@ -0,0 +1,104 @@
// 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/.
//go:build integration_api
package api
import (
"context"
"time"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/siderolabs/talos/internal/integration/base"
"github.com/siderolabs/talos/pkg/machinery/client"
"github.com/siderolabs/talos/pkg/machinery/resources/block"
)
// VolumesSuite ...
type VolumesSuite struct {
base.APISuite
ctx context.Context //nolint:containedctx
ctxCancel context.CancelFunc
}
// SuiteName ...
func (suite *VolumesSuite) SuiteName() string {
return "api.VolumesSuite"
}
// SetupTest ...
func (suite *VolumesSuite) SetupTest() {
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), time.Minute)
}
// TearDownTest ...
func (suite *VolumesSuite) TearDownTest() {
if suite.ctxCancel != nil {
suite.ctxCancel()
}
}
// TestDiscoveredVolumes verifies that standard Talos partitions are discovered.
func (suite *VolumesSuite) TestDiscoveredVolumes() {
if !suite.Capabilities().SupportsVolumes {
suite.T().Skip("cluster doesn't support volumes")
}
node := suite.RandomDiscoveredNodeInternalIP()
ctx := client.WithNode(suite.ctx, node)
volumes, err := safe.StateListAll[*block.DiscoveredVolume](ctx, suite.Client.COSI)
suite.Require().NoError(err)
expectedVolumes := map[string]struct {
Name string
}{
"META": {},
"STATE": {
Name: "xfs",
},
"EPHEMERAL": {
Name: "xfs",
},
}
for iterator := volumes.Iterator(); iterator.Next(); {
dv := iterator.Value()
partitionLabel := dv.TypedSpec().PartitionLabel
filesystemLabel := dv.TypedSpec().Label
// this is encrypted partition, skip it, we should see another device with actual filesystem
if dv.TypedSpec().Name == "luks" {
continue
}
// match either by partition or filesystem label
id := partitionLabel
expected, ok := expectedVolumes[id]
if !ok {
id = filesystemLabel
expected, ok = expectedVolumes[id]
if !ok {
continue
}
}
suite.Assert().Equal(expected.Name, dv.TypedSpec().Name)
delete(expectedVolumes, id)
}
suite.Assert().Empty(expectedVolumes)
}
func init() {
allSuites = append(allSuites, new(VolumesSuite))
}

View File

@ -152,6 +152,7 @@ type Capabilities struct {
RunsTalosKernel bool
SupportsReboot bool
SupportsRecover bool
SupportsVolumes bool
SecureBooted bool
}
@ -169,6 +170,7 @@ func (apiSuite *APISuite) Capabilities() Capabilities {
caps.RunsTalosKernel = true
caps.SupportsReboot = true
caps.SupportsRecover = true
caps.SupportsVolumes = true
}
}

View File

@ -0,0 +1,433 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.33.0
// protoc v4.25.3
// source: resource/definitions/block/block.proto
package block
import (
reflect "reflect"
sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// DeviceSpec is the spec for devices status.
type DeviceSpec struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
Major int64 `protobuf:"varint,2,opt,name=major,proto3" json:"major,omitempty"`
Minor int64 `protobuf:"varint,3,opt,name=minor,proto3" json:"minor,omitempty"`
PartitionName string `protobuf:"bytes,4,opt,name=partition_name,json=partitionName,proto3" json:"partition_name,omitempty"`
PartitionNumber int64 `protobuf:"varint,5,opt,name=partition_number,json=partitionNumber,proto3" json:"partition_number,omitempty"`
Generation int64 `protobuf:"varint,6,opt,name=generation,proto3" json:"generation,omitempty"`
DevicePath string `protobuf:"bytes,7,opt,name=device_path,json=devicePath,proto3" json:"device_path,omitempty"`
Parent string `protobuf:"bytes,8,opt,name=parent,proto3" json:"parent,omitempty"`
}
func (x *DeviceSpec) Reset() {
*x = DeviceSpec{}
if protoimpl.UnsafeEnabled {
mi := &file_resource_definitions_block_block_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DeviceSpec) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeviceSpec) ProtoMessage() {}
func (x *DeviceSpec) ProtoReflect() protoreflect.Message {
mi := &file_resource_definitions_block_block_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeviceSpec.ProtoReflect.Descriptor instead.
func (*DeviceSpec) Descriptor() ([]byte, []int) {
return file_resource_definitions_block_block_proto_rawDescGZIP(), []int{0}
}
func (x *DeviceSpec) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *DeviceSpec) GetMajor() int64 {
if x != nil {
return x.Major
}
return 0
}
func (x *DeviceSpec) GetMinor() int64 {
if x != nil {
return x.Minor
}
return 0
}
func (x *DeviceSpec) GetPartitionName() string {
if x != nil {
return x.PartitionName
}
return ""
}
func (x *DeviceSpec) GetPartitionNumber() int64 {
if x != nil {
return x.PartitionNumber
}
return 0
}
func (x *DeviceSpec) GetGeneration() int64 {
if x != nil {
return x.Generation
}
return 0
}
func (x *DeviceSpec) GetDevicePath() string {
if x != nil {
return x.DevicePath
}
return ""
}
func (x *DeviceSpec) GetParent() string {
if x != nil {
return x.Parent
}
return ""
}
// DiscoveredVolumeSpec is the spec for DiscoveredVolumes status.
type DiscoveredVolumeSpec struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Size uint64 `protobuf:"varint,1,opt,name=size,proto3" json:"size,omitempty"`
SectorSize uint64 `protobuf:"varint,2,opt,name=sector_size,json=sectorSize,proto3" json:"sector_size,omitempty"`
IoSize uint64 `protobuf:"varint,3,opt,name=io_size,json=ioSize,proto3" json:"io_size,omitempty"`
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
Uuid string `protobuf:"bytes,5,opt,name=uuid,proto3" json:"uuid,omitempty"`
Label string `protobuf:"bytes,6,opt,name=label,proto3" json:"label,omitempty"`
BlockSize uint32 `protobuf:"varint,7,opt,name=block_size,json=blockSize,proto3" json:"block_size,omitempty"`
FilesystemBlockSize uint32 `protobuf:"varint,8,opt,name=filesystem_block_size,json=filesystemBlockSize,proto3" json:"filesystem_block_size,omitempty"`
ProbedSize uint64 `protobuf:"varint,9,opt,name=probed_size,json=probedSize,proto3" json:"probed_size,omitempty"`
PartitionUuid string `protobuf:"bytes,10,opt,name=partition_uuid,json=partitionUuid,proto3" json:"partition_uuid,omitempty"`
PartitionType string `protobuf:"bytes,11,opt,name=partition_type,json=partitionType,proto3" json:"partition_type,omitempty"`
PartitionLabel string `protobuf:"bytes,12,opt,name=partition_label,json=partitionLabel,proto3" json:"partition_label,omitempty"`
PartitionIndex uint64 `protobuf:"varint,13,opt,name=partition_index,json=partitionIndex,proto3" json:"partition_index,omitempty"`
Type string `protobuf:"bytes,14,opt,name=type,proto3" json:"type,omitempty"`
DevicePath string `protobuf:"bytes,15,opt,name=device_path,json=devicePath,proto3" json:"device_path,omitempty"`
Parent string `protobuf:"bytes,16,opt,name=parent,proto3" json:"parent,omitempty"`
}
func (x *DiscoveredVolumeSpec) Reset() {
*x = DiscoveredVolumeSpec{}
if protoimpl.UnsafeEnabled {
mi := &file_resource_definitions_block_block_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DiscoveredVolumeSpec) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DiscoveredVolumeSpec) ProtoMessage() {}
func (x *DiscoveredVolumeSpec) ProtoReflect() protoreflect.Message {
mi := &file_resource_definitions_block_block_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DiscoveredVolumeSpec.ProtoReflect.Descriptor instead.
func (*DiscoveredVolumeSpec) Descriptor() ([]byte, []int) {
return file_resource_definitions_block_block_proto_rawDescGZIP(), []int{1}
}
func (x *DiscoveredVolumeSpec) GetSize() uint64 {
if x != nil {
return x.Size
}
return 0
}
func (x *DiscoveredVolumeSpec) GetSectorSize() uint64 {
if x != nil {
return x.SectorSize
}
return 0
}
func (x *DiscoveredVolumeSpec) GetIoSize() uint64 {
if x != nil {
return x.IoSize
}
return 0
}
func (x *DiscoveredVolumeSpec) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *DiscoveredVolumeSpec) GetUuid() string {
if x != nil {
return x.Uuid
}
return ""
}
func (x *DiscoveredVolumeSpec) GetLabel() string {
if x != nil {
return x.Label
}
return ""
}
func (x *DiscoveredVolumeSpec) GetBlockSize() uint32 {
if x != nil {
return x.BlockSize
}
return 0
}
func (x *DiscoveredVolumeSpec) GetFilesystemBlockSize() uint32 {
if x != nil {
return x.FilesystemBlockSize
}
return 0
}
func (x *DiscoveredVolumeSpec) GetProbedSize() uint64 {
if x != nil {
return x.ProbedSize
}
return 0
}
func (x *DiscoveredVolumeSpec) GetPartitionUuid() string {
if x != nil {
return x.PartitionUuid
}
return ""
}
func (x *DiscoveredVolumeSpec) GetPartitionType() string {
if x != nil {
return x.PartitionType
}
return ""
}
func (x *DiscoveredVolumeSpec) GetPartitionLabel() string {
if x != nil {
return x.PartitionLabel
}
return ""
}
func (x *DiscoveredVolumeSpec) GetPartitionIndex() uint64 {
if x != nil {
return x.PartitionIndex
}
return 0
}
func (x *DiscoveredVolumeSpec) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *DiscoveredVolumeSpec) GetDevicePath() string {
if x != nil {
return x.DevicePath
}
return ""
}
func (x *DiscoveredVolumeSpec) GetParent() string {
if x != nil {
return x.Parent
}
return ""
}
var File_resource_definitions_block_block_proto protoreflect.FileDescriptor
var file_resource_definitions_block_block_proto_rawDesc = []byte{
0x0a, 0x26, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e,
0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x62, 0x6c, 0x6f,
0x63, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x20, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2e,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74,
0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0xf7, 0x01, 0x0a, 0x0a, 0x44,
0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a,
0x05, 0x6d, 0x61, 0x6a, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6d, 0x61,
0x6a, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x69, 0x6e, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01,
0x28, 0x03, 0x52, 0x05, 0x6d, 0x69, 0x6e, 0x6f, 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x61, 0x72,
0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65,
0x12, 0x29, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x75,
0x6d, 0x62, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x74,
0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x67,
0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0a, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x64,
0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06,
0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61,
0x72, 0x65, 0x6e, 0x74, 0x22, 0x83, 0x04, 0x0a, 0x14, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65,
0x72, 0x65, 0x64, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x12, 0x0a,
0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x69, 0x7a,
0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x73, 0x69, 0x7a, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x73, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x53, 0x69,
0x7a, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x6f, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20,
0x01, 0x28, 0x04, 0x52, 0x06, 0x69, 0x6f, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
0x12, 0x0a, 0x04, 0x75, 0x75, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75,
0x75, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f,
0x63, 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x62,
0x6c, 0x6f, 0x63, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x66, 0x69, 0x6c, 0x65,
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x73, 0x69, 0x7a,
0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x13, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
0x74, 0x65, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1f, 0x0a, 0x0b,
0x70, 0x72, 0x6f, 0x62, 0x65, 0x64, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28,
0x04, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x62, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x25, 0x0a,
0x0e, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18,
0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e,
0x55, 0x75, 0x69, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f,
0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x61,
0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x70,
0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x0c,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4c,
0x61, 0x62, 0x65, 0x6c, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f,
0x6e, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x70,
0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x12, 0x0a,
0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70,
0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68,
0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x50, 0x61,
0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x10, 0x20, 0x01,
0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x42, 0x4a, 0x5a, 0x48, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x64, 0x65, 0x72, 0x6f, 0x6c,
0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x6d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x72, 0x79, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73,
0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_resource_definitions_block_block_proto_rawDescOnce sync.Once
file_resource_definitions_block_block_proto_rawDescData = file_resource_definitions_block_block_proto_rawDesc
)
func file_resource_definitions_block_block_proto_rawDescGZIP() []byte {
file_resource_definitions_block_block_proto_rawDescOnce.Do(func() {
file_resource_definitions_block_block_proto_rawDescData = protoimpl.X.CompressGZIP(file_resource_definitions_block_block_proto_rawDescData)
})
return file_resource_definitions_block_block_proto_rawDescData
}
var file_resource_definitions_block_block_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_resource_definitions_block_block_proto_goTypes = []interface{}{
(*DeviceSpec)(nil), // 0: talos.resource.definitions.block.DeviceSpec
(*DiscoveredVolumeSpec)(nil), // 1: talos.resource.definitions.block.DiscoveredVolumeSpec
}
var file_resource_definitions_block_block_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_resource_definitions_block_block_proto_init() }
func file_resource_definitions_block_block_proto_init() {
if File_resource_definitions_block_block_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_resource_definitions_block_block_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DeviceSpec); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_resource_definitions_block_block_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DiscoveredVolumeSpec); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_resource_definitions_block_block_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_resource_definitions_block_block_proto_goTypes,
DependencyIndexes: file_resource_definitions_block_block_proto_depIdxs,
MessageInfos: file_resource_definitions_block_block_proto_msgTypes,
}.Build()
File_resource_definitions_block_block_proto = out.File
file_resource_definitions_block_block_proto_rawDesc = nil
file_resource_definitions_block_block_proto_goTypes = nil
file_resource_definitions_block_block_proto_depIdxs = nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
// 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 provides resources related to blockdevices, mounts, etc.
package block
import (
"github.com/cosi-project/runtime/pkg/resource"
"github.com/siderolabs/talos/pkg/machinery/resources/v1alpha1"
)
//go:generate deep-copy -type DeviceSpec -type DiscoveredVolumeSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go .
// NamespaceName contains configuration resources.
const NamespaceName resource.Namespace = v1alpha1.NamespaceName

View File

@ -0,0 +1,33 @@
// 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_test
import (
"context"
"testing"
"github.com/cosi-project/runtime/pkg/resource/meta"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/cosi-project/runtime/pkg/state/registry"
"github.com/stretchr/testify/assert"
"github.com/siderolabs/talos/pkg/machinery/resources/block"
)
func TestRegisterResource(t *testing.T) {
ctx := context.TODO()
resources := state.WrapCore(namespaced.NewState(inmem.Build))
resourceRegistry := registry.NewResourceRegistry(resources)
for _, resource := range []meta.ResourceWithRD{
&block.Device{},
&block.DiscoveredVolume{},
} {
assert.NoError(t, resourceRegistry.Register(ctx, resource))
}
}

View File

@ -0,0 +1,19 @@
// 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/.
// Code generated by "deep-copy -type DeviceSpec -type DiscoveredVolumeSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT.
package block
// DeepCopy generates a deep copy of DeviceSpec.
func (o DeviceSpec) DeepCopy() DeviceSpec {
var cp DeviceSpec = o
return cp
}
// DeepCopy generates a deep copy of DiscoveredVolumeSpec.
func (o DiscoveredVolumeSpec) DeepCopy() DiscoveredVolumeSpec {
var cp DiscoveredVolumeSpec = o
return cp
}

View File

@ -0,0 +1,81 @@
// 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 (
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/meta"
"github.com/cosi-project/runtime/pkg/resource/protobuf"
"github.com/cosi-project/runtime/pkg/resource/typed"
"github.com/siderolabs/talos/pkg/machinery/proto"
)
// DeviceType is type of BlockDevice resource.
const DeviceType = resource.Type("BlockDevices.block.talos.dev")
// Device resource holds status of hardware devices (overall).
type Device = typed.Resource[DeviceSpec, DeviceExtension]
// DeviceSpec is the spec for devices status.
//
//gotagsrewrite:gen
type DeviceSpec struct {
Type string `yaml:"type" protobuf:"1"`
Major int `yaml:"major" protobuf:"2"`
Minor int `yaml:"minor" protobuf:"3"`
PartitionName string `yaml:"partitionName,omitempty" protobuf:"4"`
PartitionNumber int `yaml:"partitionNumber,omitempty" protobuf:"5"`
DevicePath string `yaml:"devicePath" protobuf:"7"`
// Generation is bumped every time the device might have changed and might need to be re-probed.
Generation int `yaml:"generation" protobuf:"6"`
// Parent (if set) specifies the parent device ID.
Parent string `yaml:"parent,omitempty" protobuf:"8"`
}
// NewDevice initializes a BlockDevice resource.
func NewDevice(namespace resource.Namespace, id resource.ID) *Device {
return typed.NewResource[DeviceSpec, DeviceExtension](
resource.NewMetadata(namespace, DeviceType, id, resource.VersionUndefined),
DeviceSpec{},
)
}
// DeviceExtension is auxiliary resource data for BlockDevice.
type DeviceExtension struct{}
// ResourceDefinition implements meta.ResourceDefinitionProvider interface.
func (DeviceExtension) ResourceDefinition() meta.ResourceDefinitionSpec {
return meta.ResourceDefinitionSpec{
Type: DeviceType,
Aliases: []resource.Type{},
DefaultNamespace: NamespaceName,
PrintColumns: []meta.PrintColumn{
{
Name: "Type",
JSONPath: `{.type}`,
},
{
Name: "PartitionName",
JSONPath: `{.partitionName}`,
},
{
Name: "Generation",
JSONPath: `{.generation}`,
},
},
}
}
func init() {
proto.RegisterDefaultTypes()
err := protobuf.RegisterDynamic[DeviceSpec](DeviceType, &Device{})
if err != nil {
panic(err)
}
}

View File

@ -0,0 +1,102 @@
// 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 (
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/meta"
"github.com/cosi-project/runtime/pkg/resource/protobuf"
"github.com/cosi-project/runtime/pkg/resource/typed"
"github.com/siderolabs/talos/pkg/machinery/proto"
)
// DiscoveredVolumeType is type of BlockDiscoveredVolume resource.
const DiscoveredVolumeType = resource.Type("DiscoveredVolumes.block.talos.dev")
// DiscoveredVolume resource holds status of hardware DiscoveredVolumes (overall).
type DiscoveredVolume = typed.Resource[DiscoveredVolumeSpec, DiscoveredVolumeExtension]
// DiscoveredVolumeSpec is the spec for DiscoveredVolumes status.
//
//gotagsrewrite:gen
type DiscoveredVolumeSpec struct {
Type string `yaml:"type" protobuf:"14"`
DevicePath string `yaml:"devicePath" protobuf:"15"`
Parent string `yaml:"parent,omitempty" protobuf:"16"`
// Overall size of the probed device (in bytes).
Size uint64 `yaml:"size" protobuf:"1"`
// Sector size of the device (in bytes).
SectorSize uint `yaml:"sectorSize,omitempty" protobuf:"2"`
// Optimal I/O size for the device (in bytes).
IOSize uint `yaml:"ioSize,omitempty" protobuf:"3"`
Name string `yaml:"name" protobuf:"4"`
UUID string `yaml:"uuid,omitempty" protobuf:"5"`
Label string `yaml:"label,omitempty" protobuf:"6"`
BlockSize uint32 `yaml:"blockSize,omitempty" protobuf:"7"`
FilesystemBlockSize uint32 `yaml:"filesystemBlockSize,omitempty" protobuf:"8"`
ProbedSize uint64 `yaml:"probedSize,omitempty" protobuf:"9"`
PartitionUUID string `yaml:"partitionUUID,omitempty" protobuf:"10"`
PartitionType string `yaml:"partitionType,omitempty" protobuf:"11"`
PartitionLabel string `yaml:"partitionLabel,omitempty" protobuf:"12"`
PartitionIndex uint `yaml:"partitionIndex,omitempty" protobuf:"13"`
}
// NewDiscoveredVolume initializes a BlockDiscoveredVolume resource.
func NewDiscoveredVolume(namespace resource.Namespace, id resource.ID) *DiscoveredVolume {
return typed.NewResource[DiscoveredVolumeSpec, DiscoveredVolumeExtension](
resource.NewMetadata(namespace, DiscoveredVolumeType, id, resource.VersionUndefined),
DiscoveredVolumeSpec{},
)
}
// DiscoveredVolumeExtension is auxiliary resource data for BlockDiscoveredVolume.
type DiscoveredVolumeExtension struct{}
// ResourceDefinition implements meta.ResourceDefinitionProvider interface.
func (DiscoveredVolumeExtension) ResourceDefinition() meta.ResourceDefinitionSpec {
return meta.ResourceDefinitionSpec{
Type: DiscoveredVolumeType,
Aliases: []resource.Type{},
DefaultNamespace: NamespaceName,
PrintColumns: []meta.PrintColumn{
{
Name: "Type",
JSONPath: `{.type}`,
},
{
Name: "Size",
JSONPath: `{.size}`,
},
{
Name: "Discovered",
JSONPath: `{.name}`,
},
{
Name: "Label",
JSONPath: `{.label}`,
},
{
Name: "PartitionLabel",
JSONPath: `{.partitionLabel}`,
},
},
}
}
func init() {
proto.RegisterDefaultTypes()
err := protobuf.RegisterDynamic[DiscoveredVolumeSpec](DiscoveredVolumeType, &DiscoveredVolume{})
if err != nil {
panic(err)
}
}

View File

@ -11,12 +11,8 @@ import (
"github.com/cosi-project/runtime/pkg/resource/typed"
"github.com/siderolabs/talos/pkg/machinery/proto"
"github.com/siderolabs/talos/pkg/machinery/resources/v1alpha1"
)
// NamespaceName contains configuration resources.
const NamespaceName resource.Namespace = v1alpha1.NamespaceName
// KernelParamSpecType is type of KernelParam resource.
const KernelParamSpecType = resource.Type("KernelParamSpecs.runtime.talos.dev")

View File

@ -4,4 +4,13 @@
package runtime
import (
"github.com/cosi-project/runtime/pkg/resource"
"github.com/siderolabs/talos/pkg/machinery/resources/v1alpha1"
)
//go:generate deep-copy -type DevicesStatusSpec -type EventSinkConfigSpec -type ExtensionServiceConfigSpec -type ExtensionServiceConfigStatusSpec -type KernelModuleSpecSpec -type KernelParamSpecSpec -type KernelParamStatusSpec -type KmsgLogConfigSpec -type MaintenanceServiceConfigSpec -type MaintenanceServiceRequestSpec -type MachineResetSignalSpec -type MachineStatusSpec -type MetaKeySpec -type MountStatusSpec -type PlatformMetadataSpec -type SecurityStateSpec -type MetaLoadedSpec -type UniqueMachineTokenSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go .
// NamespaceName contains configuration resources.
const NamespaceName resource.Namespace = v1alpha1.NamespaceName

View File

@ -25,6 +25,10 @@ description: Talos gRPC API reference.
- [File-level Extensions](#common/common.proto-extensions)
- [resource/definitions/block/block.proto](#resource/definitions/block/block.proto)
- [DeviceSpec](#talos.resource.definitions.block.DeviceSpec)
- [DiscoveredVolumeSpec](#talos.resource.definitions.block.DiscoveredVolumeSpec)
- [resource/definitions/cluster/cluster.proto](#resource/definitions/cluster/cluster.proto)
- [AffiliateSpec](#talos.resource.definitions.cluster.AffiliateSpec)
- [ConfigSpec](#talos.resource.definitions.cluster.ConfigSpec)
@ -730,6 +734,74 @@ Common metadata message nested in all reply message types
<a name="resource/definitions/block/block.proto"></a>
<p align="right"><a href="#top">Top</a></p>
## resource/definitions/block/block.proto
<a name="talos.resource.definitions.block.DeviceSpec"></a>
### DeviceSpec
DeviceSpec is the spec for devices status.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| type | [string](#string) | | |
| major | [int64](#int64) | | |
| minor | [int64](#int64) | | |
| partition_name | [string](#string) | | |
| partition_number | [int64](#int64) | | |
| generation | [int64](#int64) | | |
| device_path | [string](#string) | | |
| parent | [string](#string) | | |
<a name="talos.resource.definitions.block.DiscoveredVolumeSpec"></a>
### DiscoveredVolumeSpec
DiscoveredVolumeSpec is the spec for DiscoveredVolumes status.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| size | [uint64](#uint64) | | |
| sector_size | [uint64](#uint64) | | |
| io_size | [uint64](#uint64) | | |
| name | [string](#string) | | |
| uuid | [string](#string) | | |
| label | [string](#string) | | |
| block_size | [uint32](#uint32) | | |
| filesystem_block_size | [uint32](#uint32) | | |
| probed_size | [uint64](#uint64) | | |
| partition_uuid | [string](#string) | | |
| partition_type | [string](#string) | | |
| partition_label | [string](#string) | | |
| partition_index | [uint64](#uint64) | | |
| type | [string](#string) | | |
| device_path | [string](#string) | | |
| parent | [string](#string) | | |
<!-- end messages -->
<!-- end enums -->
<!-- end HasExtensions -->
<!-- end services -->
<a name="resource/definitions/cluster/cluster.proto"></a>
<p align="right"><a href="#top">Top</a></p>