From 3a46e08582cdef6bfb0038d75ce16d43ba527751 Mon Sep 17 00:00:00 2001 From: Mathieu Tortuyaux Date: Mon, 19 Feb 2024 16:04:34 +0100 Subject: [PATCH] platform: implement the kola elements for Scaleway Signed-off-by: Mathieu Tortuyaux --- cmd/kola/options.go | 13 +++- kola/harness.go | 5 ++ platform/machine/scaleway/cluster.go | 74 +++++++++++++++++++ platform/machine/scaleway/flight.go | 69 ++++++++++++++++++ platform/machine/scaleway/machine.go | 105 +++++++++++++++++++++++++++ 5 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 platform/machine/scaleway/cluster.go create mode 100644 platform/machine/scaleway/flight.go create mode 100644 platform/machine/scaleway/machine.go diff --git a/cmd/kola/options.go b/cmd/kola/options.go index e46be3e54..50eee6f61 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", "brightbox", "do", "esx", "external", "gce", "openstack", "equinixmetal", "qemu", "qemu-unpriv"} + kolaPlatforms = []string{"aws", "azure", "brightbox", "do", "esx", "external", "gce", "openstack", "equinixmetal", "qemu", "qemu-unpriv", "scaleway"} kolaDistros = []string{"cl", "fcos", "rhcos"} kolaChannels = []string{"alpha", "beta", "stable", "edge", "lts"} kolaOfferings = []string{"basic", "pro"} @@ -233,6 +233,16 @@ func init() { sv(&kola.BrightboxOptions.ClientSecret, "brightbox-client-secret", "", "Brightbox client secret") sv(&kola.BrightboxOptions.Image, "brightbox-image", "", "Brightbox image ref") sv(&kola.BrightboxOptions.ServerType, "brightbox-server-type", "2gb.ssd", "Brightbox server type") + + // Scaleway specific options + sv(&kola.ScalewayOptions.OrganizationID, "scaleway-organization-id", "", "Scaleway organization ID") + sv(&kola.ScalewayOptions.ProjectID, "scaleway-project-id", "", "Scaleway organization ID") + sv(&kola.ScalewayOptions.Region, "scaleway-region", "fr-par", "Scaleway region") + sv(&kola.ScalewayOptions.Zone, "scaleway-zone", "fr-par-1", "Scaleway region") + sv(&kola.ScalewayOptions.AccessKey, "scaleway-access-key", "", "Scaleway credentials access key") + sv(&kola.ScalewayOptions.SecretKey, "scaleway-secret-key", "", "Scaleway credentials secret key") + sv(&kola.ScalewayOptions.Image, "scaleway-image", "", "Scaleway image ID") + sv(&kola.ScalewayOptions.InstanceType, "scaleway-instance-type", "DEV1-S", "Scaleway instance type") } // Sync up the command line options if there is dependency @@ -252,6 +262,7 @@ func syncOptions() error { kola.EquinixMetalOptions.Board = board kola.EquinixMetalOptions.GSOptions = &kola.GCEOptions kola.BrightboxOptions.Board = board + kola.ScalewayOptions.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 09025c9b8..c9faa639a 100644 --- a/kola/harness.go +++ b/kola/harness.go @@ -46,6 +46,7 @@ import ( esxapi "github.com/flatcar/mantle/platform/api/esx" gcloudapi "github.com/flatcar/mantle/platform/api/gcloud" openstackapi "github.com/flatcar/mantle/platform/api/openstack" + scalewayapi "github.com/flatcar/mantle/platform/api/scaleway" "github.com/flatcar/mantle/platform/conf" "github.com/flatcar/mantle/platform/machine/aws" "github.com/flatcar/mantle/platform/machine/azure" @@ -57,6 +58,7 @@ import ( "github.com/flatcar/mantle/platform/machine/gcloud" "github.com/flatcar/mantle/platform/machine/openstack" "github.com/flatcar/mantle/platform/machine/qemu" + "github.com/flatcar/mantle/platform/machine/scaleway" "github.com/flatcar/mantle/platform/machine/unprivqemu" "github.com/flatcar/mantle/system" ) @@ -75,6 +77,7 @@ var ( OpenStackOptions = openstackapi.Options{Options: &Options} // glue to set platform options from main EquinixMetalOptions = equinixmetalapi.Options{Options: &Options} // glue to set platform options from main QEMUOptions = qemu.Options{Options: &Options} // glue to set platform options from main + ScalewayOptions = scalewayapi.Options{Options: &Options} // glue to set platform options from main TestParallelism int //glue var to set test parallelism from main TAPFile string // if not "", write TAP results here @@ -246,6 +249,8 @@ func NewFlight(pltfrm string) (flight platform.Flight, err error) { flight, err = qemu.NewFlight(&QEMUOptions) case "qemu-unpriv": flight, err = unprivqemu.NewFlight(&QEMUOptions) + case "scaleway": + flight, err = scaleway.NewFlight(&ScalewayOptions) default: err = fmt.Errorf("invalid platform %q", pltfrm) } diff --git a/platform/machine/scaleway/cluster.go b/platform/machine/scaleway/cluster.go new file mode 100644 index 000000000..30b92920a --- /dev/null +++ b/platform/machine/scaleway/cluster.go @@ -0,0 +1,74 @@ +// Copyright The Mantle Authors. +// SPDX-License-Identifier: Apache-2.0 + +package scaleway + +import ( + "context" + "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, nil) + if err != nil { + return nil, err + } + + instance, err := bc.flight.api.CreateServer(context.TODO(), bc.vmname(), 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, "ignition.json") + 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/scaleway/flight.go b/platform/machine/scaleway/flight.go new file mode 100644 index 000000000..c9d9126f9 --- /dev/null +++ b/platform/machine/scaleway/flight.go @@ -0,0 +1,69 @@ +// Copyright The Mantle Authors. +// SPDX-License-Identifier: Apache-2.0 + +package scaleway + +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/scaleway" +) + +const ( + Platform platform.Name = "scaleway" +) + +var ( + plog = capnslog.NewPackageLogger("github.com/flatcar/mantle", "platform/machine/scaleway") +) + +type flight struct { + *platform.BaseFlight + api *scaleway.API +} + +func NewFlight(opts *scaleway.Options) (platform.Flight, error) { + api, err := scaleway.New(opts) + if err != nil { + return nil, fmt.Errorf("creating scaleway API client: %w", err) + } + + // TODO: Rework the Base Flight to remove the CT dependency. + 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 Scaleway 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 scaleway 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/scaleway/machine.go b/platform/machine/scaleway/machine.go new file mode 100644 index 000000000..7cc0752f9 --- /dev/null +++ b/platform/machine/scaleway/machine.go @@ -0,0 +1,105 @@ +// Copyright The Mantle Authors. +// SPDX-License-Identifier: Apache-2.0 + +package scaleway + +import ( + "context" + + "golang.org/x/crypto/ssh" + + "github.com/flatcar/mantle/platform" + "github.com/flatcar/mantle/platform/api/scaleway" +) + +type machine struct { + cluster *cluster + mach *scaleway.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. +func (bm *machine) IP() string { + if bm.mach.Server.PublicIP != nil { + return bm.mach.Server.PublicIP.Address.String() + } + + return "" +} + +// IP returns the private IP of the machine. +func (bm *machine) PrivateIP() string { + if bm.mach.Server.PrivateIP != nil { + return *bm.mach.Server.PrivateIP + } + + return "" +} + +// RuntimeConf returns the runtime configuration of the cluster. +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(context.TODO(), 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 { + 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 +}