From 3f1dc1cf06d0487887017082863bd94b1a9eeccb Mon Sep 17 00:00:00 2001 From: Mathieu Tortuyaux Date: Wed, 15 Nov 2023 12:11:06 +0100 Subject: [PATCH] kola: implement brightbox platform Signed-off-by: Mathieu Tortuyaux --- cmd/kola/options.go | 8 +- kola/harness.go | 5 ++ platform/machine/brightbox/cluster.go | 80 ++++++++++++++++++ platform/machine/brightbox/flight.go | 68 +++++++++++++++ platform/machine/brightbox/machine.go | 117 ++++++++++++++++++++++++++ 5 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 platform/machine/brightbox/cluster.go create mode 100644 platform/machine/brightbox/flight.go create mode 100644 platform/machine/brightbox/machine.go diff --git a/cmd/kola/options.go b/cmd/kola/options.go index ee949be35..586b6af41 100644 --- a/cmd/kola/options.go +++ b/cmd/kola/options.go @@ -40,7 +40,7 @@ var ( kolaOffering string defaultTargetBoard = sdk.DefaultBoard() kolaArchitectures = []string{"amd64"} - kolaPlatforms = []string{"aws", "azure", "do", "esx", "external", "gce", "openstack", "equinixmetal", "qemu", "qemu-unpriv"} + kolaPlatforms = []string{"aws", "azure", "brightbox", "do", "esx", "external", "gce", "openstack", "equinixmetal", "qemu", "qemu-unpriv"} kolaDistros = []string{"cl", "fcos", "rhcos"} kolaChannels = []string{"alpha", "beta", "stable", "edge", "lts"} kolaOfferings = []string{"basic", "pro"} @@ -227,6 +227,11 @@ func init() { sv(&kola.QEMUOptions.BIOSImage, "qemu-bios", "", "BIOS to use for QEMU vm") bv(&kola.QEMUOptions.UseVanillaImage, "qemu-skip-mangle", false, "don't modify CL disk image to capture console log") sv(&kola.QEMUOptions.ExtraBaseDiskSize, "qemu-grow-base-disk-by", "", "grow base disk by the given size in bytes, following optional 1024-based suffixes are allowed: b (ignored), k, K, M, G, T") + + // BrightBox specific options + sv(&kola.BrightboxOptions.ClientID, "brightbox-client-id", "", "Brightbox client ID") + sv(&kola.BrightboxOptions.ClientSecret, "brightbox-client-secret", "", "Brightbox client secret") + sv(&kola.BrightboxOptions.Image, "brightbox-image", "", "Brightbox image ref") } // Sync up the command line options if there is dependency @@ -245,6 +250,7 @@ func syncOptions() error { kola.AWSOptions.Board = board kola.EquinixMetalOptions.Board = board kola.EquinixMetalOptions.GSOptions = &kola.GCEOptions + kola.BrightboxOptions.Board = board validateOption := func(name, item string, valid []string) error { for _, v := range valid { diff --git a/kola/harness.go b/kola/harness.go index 349bb74d9..09025c9b8 100644 --- a/kola/harness.go +++ b/kola/harness.go @@ -40,6 +40,7 @@ import ( "github.com/flatcar/mantle/platform" awsapi "github.com/flatcar/mantle/platform/api/aws" azureapi "github.com/flatcar/mantle/platform/api/azure" + brightboxapi "github.com/flatcar/mantle/platform/api/brightbox" doapi "github.com/flatcar/mantle/platform/api/do" equinixmetalapi "github.com/flatcar/mantle/platform/api/equinixmetal" esxapi "github.com/flatcar/mantle/platform/api/esx" @@ -48,6 +49,7 @@ import ( "github.com/flatcar/mantle/platform/conf" "github.com/flatcar/mantle/platform/machine/aws" "github.com/flatcar/mantle/platform/machine/azure" + "github.com/flatcar/mantle/platform/machine/brightbox" "github.com/flatcar/mantle/platform/machine/do" "github.com/flatcar/mantle/platform/machine/equinixmetal" "github.com/flatcar/mantle/platform/machine/esx" @@ -65,6 +67,7 @@ var ( Options = platform.Options{} AWSOptions = awsapi.Options{Options: &Options} // glue to set platform options from main AzureOptions = azureapi.Options{Options: &Options} // glue to set platform options from main + BrightboxOptions = brightboxapi.Options{Options: &Options} // glue to set platform options from main DOOptions = doapi.Options{Options: &Options} // glue to set platform options from main ESXOptions = esxapi.Options{Options: &Options} // glue to set platform options from main ExternalOptions = external.Options{Options: &Options} // glue to set platform options from main @@ -225,6 +228,8 @@ func NewFlight(pltfrm string) (flight platform.Flight, err error) { flight, err = aws.NewFlight(&AWSOptions) case "azure": flight, err = azure.NewFlight(&AzureOptions) + case "brightbox": + flight, err = brightbox.NewFlight(&BrightboxOptions) case "do": flight, err = do.NewFlight(&DOOptions) case "esx": diff --git a/platform/machine/brightbox/cluster.go b/platform/machine/brightbox/cluster.go new file mode 100644 index 000000000..430759a4e --- /dev/null +++ b/platform/machine/brightbox/cluster.go @@ -0,0 +1,80 @@ +// Copyright The Mantle Authors. +// SPDX-License-Identifier: Apache-2.0 + +package brightbox + +import ( + "crypto/rand" + "fmt" + "os" + "path/filepath" + + "github.com/flatcar/mantle/platform" + "github.com/flatcar/mantle/platform/conf" +) + +type cluster struct { + *platform.BaseCluster + flight *flight +} + +func (bc *cluster) NewMachine(userdata *conf.UserData) (platform.Machine, error) { + conf, err := bc.RenderUserData(userdata, map[string]string{ + "$public_ipv4": "${COREOS_OPENSTACK_IPV4_PUBLIC}", + "$private_ipv4": "${COREOS_OPENSTACK_IPV4_LOCAL}", + }) + if err != nil { + return nil, err + } + + var keyname string + if !bc.RuntimeConf().NoSSHKeyInMetadata { + keyname = bc.flight.Name() + } + instance, err := bc.flight.api.CreateServer(bc.vmname(), keyname, conf.String()) + if err != nil { + return nil, err + } + + mach := &machine{ + cluster: bc, + mach: instance, + } + + mach.dir = filepath.Join(bc.RuntimeConf().OutputDir, mach.ID()) + if err := os.Mkdir(mach.dir, 0777); err != nil { + mach.Destroy() + return nil, err + } + + confPath := filepath.Join(mach.dir, "user-data") + if err := conf.WriteFile(confPath); err != nil { + mach.Destroy() + return nil, err + } + + if mach.journal, err = platform.NewJournal(mach.dir); err != nil { + mach.Destroy() + return nil, err + } + + if err := platform.StartMachine(mach, mach.journal); err != nil { + mach.Destroy() + return nil, err + } + + bc.AddMach(mach) + + return mach, nil +} + +func (bc *cluster) vmname() string { + b := make([]byte, 5) + rand.Read(b) + return fmt.Sprintf("%s-%x", bc.Name()[0:13], b) +} + +func (bc *cluster) Destroy() { + bc.BaseCluster.Destroy() + bc.flight.DelCluster(bc) +} diff --git a/platform/machine/brightbox/flight.go b/platform/machine/brightbox/flight.go new file mode 100644 index 000000000..a36ad409e --- /dev/null +++ b/platform/machine/brightbox/flight.go @@ -0,0 +1,68 @@ +// Copyright The Mantle Authors. +// SPDX-License-Identifier: Apache-2.0 + +package brightbox + +import ( + "fmt" + + "github.com/coreos/pkg/capnslog" + ctplatform "github.com/flatcar/container-linux-config-transpiler/config/platform" + + "github.com/flatcar/mantle/platform" + "github.com/flatcar/mantle/platform/api/brightbox" +) + +const ( + Platform platform.Name = "brightbox" +) + +var ( + plog = capnslog.NewPackageLogger("github.com/flatcar/mantle", "platform/machine/brightbox") +) + +type flight struct { + *platform.BaseFlight + api *brightbox.API +} + +func NewFlight(opts *brightbox.Options) (platform.Flight, error) { + api, err := brightbox.New(opts) + if err != nil { + return nil, fmt.Errorf("creating brightbox API client: %w", err) + } + + base, err := platform.NewBaseFlight(opts.Options, Platform, ctplatform.OpenStackMetadata) + if err != nil { + return nil, fmt.Errorf("creating base flight: %w", err) + } + + bf := &flight{ + BaseFlight: base, + api: api, + } + + return bf, nil +} + +// NewCluster creates an instance of a Cluster suitable for spawning +// instances on the OpenStack platform. +func (bf *flight) NewCluster(rconf *platform.RuntimeConfig) (platform.Cluster, error) { + bc, err := platform.NewBaseCluster(bf.BaseFlight, rconf) + if err != nil { + return nil, fmt.Errorf("creating brightbox base cluster: %w", err) + } + + c := &cluster{ + BaseCluster: bc, + flight: bf, + } + + bf.AddCluster(c) + + return c, nil +} + +func (bf *flight) Destroy() { + bf.BaseFlight.Destroy() +} diff --git a/platform/machine/brightbox/machine.go b/platform/machine/brightbox/machine.go new file mode 100644 index 000000000..0f4cf6b6a --- /dev/null +++ b/platform/machine/brightbox/machine.go @@ -0,0 +1,117 @@ +// Copyright The Mantle Authors. +// SPDX-License-Identifier: Apache-2.0 +package brightbox + +import ( + "fmt" + "os" + "path/filepath" + + "golang.org/x/crypto/ssh" + + "github.com/flatcar/mantle/platform" + "github.com/flatcar/mantle/platform/api/brightbox" +) + +type machine struct { + cluster *cluster + mach *brightbox.Server + dir string + journal *platform.Journal + console string +} + +// ID returns the ID of the machine. +func (bm *machine) ID() string { + return bm.mach.Server.ID +} + +// IP returns the IP of the machine. +// The machine should only get one "cloud" IP. +func (bm *machine) IP() string { + if bm.mach.Server != nil && len(bm.mach.Server.CloudIPs) >= 0 { + return bm.mach.Server.CloudIPs[0].PublicIPv4 + } + + return "" +} + +func (bm *machine) PrivateIP() string { + // TODO: We can see a Private IP from the dasbhboard but I don't know how to get it + // from the API. + return "" +} + +func (bm *machine) RuntimeConf() *platform.RuntimeConfig { + return bm.cluster.RuntimeConf() +} + +func (bm *machine) SSHClient() (*ssh.Client, error) { + return bm.cluster.SSHClient(bm.IP()) +} + +func (bm *machine) PasswordSSHClient(user string, password string) (*ssh.Client, error) { + return bm.cluster.PasswordSSHClient(bm.IP(), user, password) +} + +func (bm *machine) SSH(cmd string) ([]byte, []byte, error) { + return bm.cluster.SSH(bm, cmd) +} + +func (bm *machine) Reboot() error { + return platform.RebootMachine(bm, bm.journal) +} + +func (bm *machine) Destroy() { + if err := bm.saveConsole(); err != nil { + plog.Errorf("Error saving console for instance %v: %v", bm.ID(), err) + } + + if err := bm.cluster.flight.api.DeleteServer(bm.ID()); err != nil { + plog.Errorf("deleting server %v: %v", bm.ID(), err) + } + + if bm.journal != nil { + bm.journal.Destroy() + } + + bm.cluster.DelMach(bm) +} + +func (bm *machine) ConsoleOutput() string { + return bm.console +} + +func (bm *machine) saveConsole() error { + var err error + bm.console, err = bm.cluster.flight.api.GetConsoleOutput(bm.ID()) + if err != nil { + return fmt.Errorf("Error retrieving console log for %v: %v", bm.ID(), err) + } + + path := filepath.Join(bm.dir, "console.txt") + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + defer f.Close() + f.WriteString(bm.console) + + return nil +} + +func (bm *machine) JournalOutput() string { + if bm.journal == nil { + return "" + } + + data, err := bm.journal.Read() + if err != nil { + plog.Errorf("Reading journal for instance %v: %v", bm.ID(), err) + } + return string(data) +} + +func (bm *machine) Board() string { + return bm.cluster.flight.Options().Board +}