Skip to content

Commit

Permalink
feat: implement blockdevice watch controller
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
smira committed Mar 18, 2024
1 parent 06e3bc0 commit 15beb14
Show file tree
Hide file tree
Showing 28 changed files with 3,220 additions and 6 deletions.
3 changes: 1 addition & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 38 additions & 0 deletions api/resource/definitions/block/block.proto
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
6 changes: 6 additions & 0 deletions internal/app/machined/pkg/controllers/block/block.go
Original file line number Diff line number Diff line change
@@ -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
246 changes: 246 additions & 0 deletions internal/app/machined/pkg/controllers/block/devices.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 15beb14

Please sign in to comment.