Skip to content

Commit

Permalink
Implement device-selector
Browse files Browse the repository at this point in the history
This commit adds the implementation for picking the installation device
during registration based on a deviceSelector in the
MachineRegistration.

Signed-off-by: Fredrik Lönnegren <[email protected]>
  • Loading branch information
frelon committed Nov 15, 2023
1 parent 2ed2a84 commit f352761
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 8 deletions.
8 changes: 8 additions & 0 deletions charts/crds/templates/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -673,8 +673,16 @@ spec:
items:
properties:
key:
enum:
- Name
- Size
type: string
operator:
enum:
- In
- NotIn
- Gt
- Lt
type: string
values:
items:
Expand Down
8 changes: 7 additions & 1 deletion cmd/register/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"time"

"github.com/jaypipes/ghw"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/twpayne/go-vfs"
Expand Down Expand Up @@ -57,7 +58,12 @@ var (

func main() {
fs := vfs.OSFS
installer := install.NewInstaller(fs)
blockInfo, err := ghw.Block(ghw.WithDisableWarnings())
if err != nil {
log.Warningf("error probing disks: %s", err)
}

installer := install.NewInstaller(fs, blockInfo.Disks)
stateHandler := register.NewFileStateHandler(fs)
client := register.NewClient()
cmd := newCommand(fs, client, stateHandler, installer)
Expand Down
122 changes: 115 additions & 7 deletions pkg/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,23 @@ import (
"os"
"path/filepath"

"github.com/jaypipes/ghw"
"github.com/jaypipes/ghw/pkg/block"
"github.com/mudler/yip/pkg/schema"
elementalv1 "github.com/rancher/elemental-operator/api/v1beta1"
"github.com/rancher/elemental-operator/controllers"
"github.com/rancher/elemental-operator/pkg/elementalcli"
"github.com/rancher/elemental-operator/pkg/log"
"github.com/rancher/elemental-operator/pkg/register"
"github.com/rancher/elemental-operator/pkg/util"
agent "github.com/rancher/system-agent/pkg/config"
"github.com/twpayne/go-vfs"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"

elementalv1 "github.com/rancher/elemental-operator/api/v1beta1"
"github.com/rancher/elemental-operator/controllers"
"github.com/rancher/elemental-operator/pkg/elementalcli"
"github.com/rancher/elemental-operator/pkg/log"
"github.com/rancher/elemental-operator/pkg/register"
"github.com/rancher/elemental-operator/pkg/util"
)

const (
Expand All @@ -59,9 +63,10 @@ type Installer interface {
WriteLocalSystemAgentConfig(config elementalv1.Elemental) error
}

func NewInstaller(fs vfs.FS) Installer {
func NewInstaller(fs vfs.FS, disks []*block.Disk) Installer {
return &installer{
fs: fs,
disks: disks,
runner: elementalcli.NewRunner(),
}
}
Expand All @@ -70,6 +75,7 @@ var _ Installer = (*installer)(nil)

type installer struct {
fs vfs.FS
disks []*block.Disk
runner elementalcli.Runner
}

Expand All @@ -78,6 +84,15 @@ func (i *installer) InstallElemental(config elementalv1.Config, state register.S
config.Elemental.Install.ConfigURLs = []string{}
}

if config.Elemental.Install.Device == "" {
deviceName, err := i.findInstallationDevice(config.Elemental.Install.DeviceSelector)
if err != nil {
return fmt.Errorf("failed picking installation device: %w", err)
}

config.Elemental.Install.Device = deviceName
}

additionalConfigs, err := i.getCloudInitConfigs(config, state)
if err != nil {
return fmt.Errorf("generating additional cloud configs: %w", err)
Expand Down Expand Up @@ -116,6 +131,99 @@ func (i *installer) ResetElemental(config elementalv1.Config, state register.Sta
return nil
}

func (i *installer) findInstallationDevice(selector elementalv1.DeviceSelector) (string, error) {
devices := map[string]*ghw.Disk{}

for _, disk := range i.disks {
devices[disk.Name] = disk
}

for _, disk := range i.disks {
for _, sel := range selector {
matches, err := matches(disk, sel)
if err != nil {
return "", err
}

if !matches {
log.Debug("%s does not match selector %s", disk.Name, sel.Key)
delete(devices, disk.Name)
break
}
}
}

log.Debug("%s disks matching selector", len(devices))

for _, dev := range devices {
return dev.Name, nil
}

return "", fmt.Errorf("no device found matching selector")
}

func matches(disk *block.Disk, req elementalv1.DeviceSelectorRequirement) (bool, error) {
switch req.Operator {
case elementalv1.DeviceSelectorOpIn:
return matchesIn(disk, req)
case elementalv1.DeviceSelectorOpNotIn:
return matchesNotIn(disk, req)
case elementalv1.DeviceSelectorOpLt:
return matchesLt(disk, req)
case elementalv1.DeviceSelectorOpGt:
return matchesGt(disk, req)
default:
return false, fmt.Errorf("unknown operator: %s", req.Operator)
}
}

func matchesIn(disk *block.Disk, req elementalv1.DeviceSelectorRequirement) (bool, error) {
if req.Key != elementalv1.DeviceSelectorKeyName {
return false, fmt.Errorf("cannot use In operator on numerical values %s", req.Key)
}

for _, val := range req.Values {
if val == disk.Name {
return true, nil
}
}

return false, nil
}
func matchesNotIn(disk *block.Disk, req elementalv1.DeviceSelectorRequirement) (bool, error) {
matches, err := matchesIn(disk, req)
return !matches, err
}
func matchesLt(disk *block.Disk, req elementalv1.DeviceSelectorRequirement) (bool, error) {
if req.Key != elementalv1.DeviceSelectorKeySize {
return false, fmt.Errorf("cannot use Lt operator on string values %s", req.Key)

}

keySize, err := resource.ParseQuantity(req.Values[0])
if err != nil {
return false, fmt.Errorf("failed to parse quantity %s", req.Values[0])
}

diskSize := resource.NewQuantity(int64(disk.SizeBytes), resource.BinarySI)

return diskSize.Cmp(keySize) == -1, nil
}
func matchesGt(disk *block.Disk, req elementalv1.DeviceSelectorRequirement) (bool, error) {
if req.Key != elementalv1.DeviceSelectorKeySize {
return false, fmt.Errorf("cannot use Gt operator on string values %s", req.Key)
}

keySize, err := resource.ParseQuantity(req.Values[0])
if err != nil {
return false, fmt.Errorf("failed to parse quantity %s", req.Values[0])
}

diskSize := resource.NewQuantity(int64(disk.SizeBytes), resource.BinarySI)

return diskSize.Cmp(keySize) == 1, nil
}

// getCloudInitConfigs creates cloud-init configuration files that can be passed as additional `config-urls`
// to the `elemental` cli. We exploit this mechanism to persist information during `elemental install`
// or `elemental reset` calls into the newly installed or resetted system.
Expand Down
124 changes: 124 additions & 0 deletions pkg/install/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"testing"
"time"

"github.com/jaypipes/ghw"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/twpayne/go-vfs"
Expand Down Expand Up @@ -122,6 +123,129 @@ var _ = Describe("installer install elemental", Label("installer", "install"), f
})
})

var _ = Describe("installer pick device", Label("installer", "install", "device", "disk"), func() {
var fs *vfst.TestFS
var err error
var fsCleanup func()
var cliRunner *climocks.MockRunner
var install *installer
BeforeEach(func() {
fs, fsCleanup, err = vfst.NewTestFS(map[string]interface{}{"/tmp/init": ""})
Expect(err).ToNot(HaveOccurred())
mockCtrl := gomock.NewController(GinkgoT())
cliRunner = climocks.NewMockRunner(mockCtrl)
DeferCleanup(fsCleanup)
})
It("should pick single device no selectors", func() {
install = &installer{
fs: fs,
runner: cliRunner,
disks: []*ghw.Disk{{Name: "/dev/pickme"}},
}
actualDevice, err := install.findInstallationDevice(elementalv1.DeviceSelector{})
Expect(err).ToNot(HaveOccurred())
Expect(actualDevice).To(Equal("/dev/pickme"))
})
It("should pick device based on selector name", func() {
install = &installer{
fs: fs,
runner: cliRunner,
disks: []*ghw.Disk{
{Name: "/dev/sda"},
{Name: "/dev/sdb"},
{Name: "/dev/sdc"},
{Name: "/dev/sdd"},
{Name: "/dev/sde"},
{Name: "/dev/sdf"},
{Name: "/dev/sdg"},
},
}
selector := elementalv1.DeviceSelector{
{
Key: elementalv1.DeviceSelectorKeyName,
Operator: elementalv1.DeviceSelectorOpIn,
Values: []string{"/dev/sdd"},
},
}

actualDevice, err := install.findInstallationDevice(selector)
Expect(err).ToNot(HaveOccurred())
Expect(actualDevice).To(Equal("/dev/sdd"))
})
It("should pick device less than 100Gi", func() {
install = &installer{
fs: fs,
runner: cliRunner,
disks: []*ghw.Disk{
{Name: "/dev/sda", SizeBytes: 85899345920},
{Name: "/dev/sdb", SizeBytes: 214748364800},
},
}
selector := elementalv1.DeviceSelector{
{
Key: elementalv1.DeviceSelectorKeySize,
Operator: elementalv1.DeviceSelectorOpLt,
Values: []string{"100Gi"},
},
}

actualDevice, err := install.findInstallationDevice(selector)
Expect(err).ToNot(HaveOccurred())
Expect(actualDevice).To(Equal("/dev/sda"))
})
It("should pick device greater than 100Gi", func() {
install = &installer{
fs: fs,
runner: cliRunner,
disks: []*ghw.Disk{
{Name: "/dev/sda", SizeBytes: 85899345920},
{Name: "/dev/sdb", SizeBytes: 214748364800},
},
}
selector := elementalv1.DeviceSelector{
{
Key: elementalv1.DeviceSelectorKeySize,
Operator: elementalv1.DeviceSelectorOpGt,
Values: []string{"100Gi"},
},
}

actualDevice, err := install.findInstallationDevice(selector)
Expect(err).ToNot(HaveOccurred())
Expect(actualDevice).To(Equal("/dev/sdb"))
})
It("should not error out for 2 matching devices", func() {
install = &installer{
fs: fs,
runner: cliRunner,
disks: []*ghw.Disk{
{Name: "/dev/sda"},
{Name: "/dev/sdb"},
},
}
selector := elementalv1.DeviceSelector{
{
Key: elementalv1.DeviceSelectorKeyName,
Operator: elementalv1.DeviceSelectorOpIn,
Values: []string{"/dev/sda", "/dev/sdb"},
},
}
actualDevice, err := install.findInstallationDevice(selector)
Expect(err).ToNot(HaveOccurred())
Expect(actualDevice).ToNot(BeEmpty())
})
It("should error out for no devices", func() {
install = &installer{
fs: fs,
runner: cliRunner,
disks: []*ghw.Disk{},
}
actualDevice, err := install.findInstallationDevice(elementalv1.DeviceSelector{})
Expect(err).To(HaveOccurred())
Expect(actualDevice).To(BeEmpty())
})
})

var _ = Describe("installer reset elemental", Label("installer", "reset"), func() {
var fs *vfst.TestFS
var err error
Expand Down

0 comments on commit f352761

Please sign in to comment.