From e6d614f4dd7350624d4b2cd45624d0a85e02cee9 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Fri, 3 Jan 2025 18:19:31 -0500 Subject: [PATCH] engine: resources: Add a bmc resource This resource manages bmc devices in servers or elsewhere. This also integrates with the provisioner code. --- engine/resources/bmc_power.go | 466 ++++++++++++++++++ examples/lang/bmc-power.mcl | 12 + examples/mockbmc/fixtures/service_root.json | 45 ++ examples/mockbmc/fixtures/session_delete.json | 6 + .../mockbmc/fixtures/session_service.json | 10 + examples/mockbmc/fixtures/systems.json | 13 + examples/mockbmc/fixtures/systems_1.json.tmpl | 107 ++++ examples/mockbmc/mockbmc.go | 297 +++++++++++ go.mod | 20 +- go.sum | 29 ++ lang/core/embedded/provisioner/main.mcl | 12 + lang/core/embedded/provisioner/provisioner.go | 4 + lang/core/embedded/provisioner/top.mcl | 1 + 13 files changed, 1017 insertions(+), 5 deletions(-) create mode 100644 engine/resources/bmc_power.go create mode 100644 examples/lang/bmc-power.mcl create mode 100644 examples/mockbmc/fixtures/service_root.json create mode 100644 examples/mockbmc/fixtures/session_delete.json create mode 100644 examples/mockbmc/fixtures/session_service.json create mode 100644 examples/mockbmc/fixtures/systems.json create mode 100644 examples/mockbmc/fixtures/systems_1.json.tmpl create mode 100644 examples/mockbmc/mockbmc.go diff --git a/engine/resources/bmc_power.go b/engine/resources/bmc_power.go new file mode 100644 index 0000000000..4ff4edc452 --- /dev/null +++ b/engine/resources/bmc_power.go @@ -0,0 +1,466 @@ +// Mgmt +// Copyright (C) 2013-2024+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this program, or any covered work, by linking or combining it +// with embedded mcl code and modules (and that the embedded mcl code and +// modules which link with this program, contain a copy of their source code in +// the authoritative form) containing parts covered by the terms of any other +// license, the licensors of this program grant you additional permission to +// convey the resulting work. Furthermore, the licensors of this program grant +// the original author, James Shubin, additional permission to update this +// additional permission if he deems it necessary to achieve the goals of this +// additional permission. + +package resources + +import ( + "context" + "fmt" + "net" + "net/url" + "strconv" + "strings" + + "github.com/purpleidea/mgmt/engine" + "github.com/purpleidea/mgmt/engine/traits" + + bmclib "github.com/bmc-toolbox/bmclib/v2" + "github.com/bmc-toolbox/bmclib/v2/providers/rpc" +) + +func init() { + engine.RegisterResource("bmc:power", func() engine.Res { return &BmcPowerRes{} }) +} + +const ( + // DefaultBmcPowerPort is the default port we try to connect on. + DefaultBmcPowerPort = 443 + + // BmcDriverSecureSuffix is the magic char we append to a driver name to + // specify we want the SSL/TLS variant. + BmcDriverSecureSuffix = "s" + + // BmcDriverRPC is the RPC driver. + BmcDriverRPC = "rpc" + + // BmcDriverGofish is the gofish driver. + BmcDriverGofish = "gofish" +) + +// BmcPowerRes is a resource that manages power state of a BMC. This is usually +// used for turning computers on and off. The name value can be a big URL string +// in the form: `driver://user:pass@hostname:port` for example you may see: +// gofishs://ADMIN:hunter2@127.0.0.1:8800 to use the "https" variant of the +// gofish driver. +// +// NOTE: New drivers should either not end in "s" or at least not be identical +// to the name of another driver an "s" is added or removed to the end. +type BmcPowerRes struct { + traits.Base // add the base methods without re-implementation + + init *engine.Init + + // Hostname to connect to. If not specified, we parse this from the + // Name. + Hostname string `lang:"hostname" yaml:"hostname"` + + // Port to connect to. If not specified, we parse this from the Name. + Port int `lang:"port" yaml:"port"` + + // Username to use to connect. If not specified, we parse this from the + // Name. + // TODO: If the Username field is not set, should we parse from the + // Name? It's not really part of the BMC unique identifier so maybe we + // shouldn't use that. + Username string `lang:"username" yaml:"username"` + + // Password to use to connect. We do NOT parse this from the Name unless + // you set InsecurePassword to true. + // XXX: Use mgmt magic credentials in the future. + Password string `lang:"password" yaml:"password"` + + // InsecurePassword can be set to true to allow a password in the Name. + InsecurePassword bool `lang:"insecure_password" yaml:"insecure_password"` + + // Driver to use, such as: "gofish" or "rpc". This is a different + // concept than the "bmclib" driver vs provider distinction. Here we + // just statically pick what we're using without any magic. If not + // specified, we parse this from the Name scheme. If this ends with an + // extra "s" then we use https instead of http. + Driver string `lang:"driver" yaml:"driver"` + + // State of machine power. Can be "on" or "off". + State string `lang:"state" yaml:"state"` + + driver string + scheme string +} + +// validDriver determines if we are using a valid drive. This does not include +// the magic "s" bits. This function need to be expanded as we support more +// drivers. +func (obj *BmcPowerRes) validDriver(driver string) error { + if driver == BmcDriverRPC { + return nil + } + if driver == BmcDriverGofish { + return nil + } + + return fmt.Errorf("unknown driver: %s", driver) +} + +// getHostname returns the hostname that we want to connect to. If the Hostname +// field is set, we use that, otherwise we parse from the Name. +func (obj *BmcPowerRes) getHostname() string { + if obj.Hostname != "" { + return obj.Hostname + } + + u, err := url.Parse(obj.Name()) + if err != nil || u == nil { + return "" + } + + // SplitHostPort splits a network address of the form "host:port", + // "host%zone:port", "[host]:port" or "[host%zone]:port" into host or + // host%zone and port. + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return u.Host // must be a naked hostname or ip w/o port + } + _ = port + + return host +} + +// getPort returns the port that we want to connect to. If the Port field is +// set, we use that, otherwise we parse from the Name. +// +// NOTE: We return a string since all the bmclib things usually expect a string, +// but if that gets fixed we should return an int here instead. +func (obj *BmcPowerRes) getPort() string { + if obj.Port != 0 { + return strconv.Itoa(obj.Port) + } + + u, err := url.Parse(obj.Name()) + if err != nil || u == nil { + return "" + } + + // SplitHostPort splits a network address of the form "host:port", + // "host%zone:port", "[host]:port" or "[host%zone]:port" into host or + // host%zone and port. + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return strconv.Itoa(DefaultBmcPowerPort) // default port + } + _ = host + + return port +} + +// getUsername returns the username that we want to connect with. If the +// Username field is set, we use that, otherwise we parse from the Name. +// TODO: If the Username field is not set, should we parse from the Name? It's +// not really part of the BMC unique identifier so maybe we shouldn't use that. +func (obj *BmcPowerRes) getUsername() string { + if obj.Username != "" { + return obj.Username + } + + u, err := url.Parse(obj.Name()) + if err != nil || u == nil || u.User == nil { + return "" + } + + return u.User.Username() +} + +// getPassword returns the password that we want to connect with. +// XXX: Use mgmt magic credentials in the future. +func (obj *BmcPowerRes) getPassword() string { + if obj.Password != "" || !obj.InsecurePassword { + return obj.Password + } + // NOTE: We don't look at any password string from the name unless the + // InsecurePassword field is true. + + u, err := url.Parse(obj.Name()) + if err != nil || u == nil || u.User == nil { + return "" + } + + password, ok := u.User.Password() + if !ok { + return "" + } + + return password +} + +// getRawDriver returns the raw magic driver string. If the Driver field is set, +// we use that, otherwise we parse from the Name. This version may include the +// magic "s" at the end. +func (obj *BmcPowerRes) getRawDriver() string { + if obj.Driver != "" { + return obj.Driver + } + + u, err := url.Parse(obj.Name()) + if err != nil || u == nil { + return "" + } + + return u.Scheme +} + +// getDriverAndScheme figures out which driver and scheme we want to use. +func (obj *BmcPowerRes) getDriverAndScheme() (string, string, error) { + driver := obj.getRawDriver() + err := obj.validDriver(driver) + if err == nil { + return driver, "http", nil + } + + driver = strings.TrimSuffix(driver, BmcDriverSecureSuffix) + if err := obj.validDriver(driver); err == nil { + return driver, "https", nil + } + + return "", "", err // return the first error +} + +// getDriver returns the actual driver that we want to connect with. If the +// Driver field is set, we use that, otherwise we parse from the Name. This +// version does NOT include the magic "s" at the end. +func (obj *BmcPowerRes) getDriver() string { + return obj.driver +} + +// getScheme figures out which scheme we want to use. +func (obj *BmcPowerRes) getScheme() string { + return obj.scheme +} + +// Default returns some sensible defaults for this resource. +func (obj *BmcPowerRes) Default() engine.Res { + return &BmcPowerRes{} +} + +// Validate if the params passed in are valid data. +func (obj *BmcPowerRes) Validate() error { + // XXX: Force polling until we have real events... + if obj.MetaParams().Poll == 0 { + return fmt.Errorf("events are not yet supported, use polling") + } + + if obj.getHostname() == "" { + return fmt.Errorf("need a Hostname") + } + //if obj.getUsername() == "" { + // return fmt.Errorf("need a Username") + //} + + if obj.getRawDriver() == "" { + return fmt.Errorf("need a Driver") + } + if _, _, err := obj.getDriverAndScheme(); err != nil { + return err + } + + return nil +} + +// Init runs some startup code for this resource. +func (obj *BmcPowerRes) Init(init *engine.Init) error { + obj.init = init // save for later + + driver, scheme, err := obj.getDriverAndScheme() + if err != nil { + // programming error (we checked in Validate) + return err + } + obj.driver = driver + obj.scheme = scheme + + return nil +} + +// Cleanup is run by the engine to clean up after the resource is done. +func (obj *BmcPowerRes) Cleanup() error { + return nil +} + +// client builds the bmclib client. The API to build it is complicated. +func (obj *BmcPowerRes) client() *bmclib.Client { + // NOTE: The bmclib API is weird, you can't put the port in this string! + u := fmt.Sprintf("%s://%s", obj.getScheme(), obj.getHostname()) + + uPort := u + if p := obj.getPort(); p != "" { + uPort = u + ":" + p + } + + opts := []bmclib.Option{} + + if obj.getDriver() == BmcDriverRPC { + opts = append(opts, bmclib.WithRPCOpt(rpc.Provider{ + // NOTE: The main API cannot take a port, but here we do! + ConsumerURL: uPort, + })) + } + + if p := obj.getPort(); p != "" { + switch obj.getDriver() { + case BmcDriverRPC: + // TODO: ??? + + case BmcDriverGofish: + // XXX: Why doesn't this accept an int? + opts = append(opts, bmclib.WithRedfishPort(p)) + + //case BmcDriverOpenbmc: + // // XXX: Why doesn't this accept an int? + // opts = append(opts, openbmc.WithPort(p)) + + default: + // TODO: error or pass through? + obj.init.Logf("unhandled driver: %s", obj.getDriver()) + } + } + + client := bmclib.NewClient(u, obj.getUsername(), obj.Password, opts...) + + if obj.getDriver() != "" && obj.getDriver() != BmcDriverRPC { + client = client.For(obj.getDriver()) // limit to this provider + } + + return client +} + +// Watch is the primary listener for this resource and it outputs events. +func (obj *BmcPowerRes) Watch(ctx context.Context) error { + obj.init.Running() // when started, notify engine that we're running + + select { + case <-ctx.Done(): // closed by the engine to signal shutdown + } + + //obj.init.Event() // notify engine of an event (this can block) + + return nil +} + +// CheckApply method for BmcPower resource. Does nothing, returns happy! +func (obj *BmcPowerRes) CheckApply(ctx context.Context, apply bool) (bool, error) { + + client := obj.client() + + if err := client.Open(ctx); err != nil { + return false, err + } + defer client.Close(ctx) // (err error) + + if obj.init.Debug { + obj.init.Logf("connected ok") + } + + state, err := client.GetPowerState(ctx) + if err != nil { + return false, err + } + state = strings.ToLower(state) // normalize + obj.init.Logf("get state: %s", state) + + if !apply { + return false, nil + } + + if obj.State == state { + return true, nil + } + + // TODO: should this be "On" and "Off"? Does case matter? + ok, err := client.SetPowerState(ctx, obj.State) + if err != nil { + return false, err + } + if !ok { + // TODO: When is this ever false? + } + obj.init.Logf("set state: %s", obj.State) + + return false, nil +} + +// Cmp compares two resources and returns an error if they are not equivalent. +func (obj *BmcPowerRes) Cmp(r engine.Res) error { + // we can only compare BmcPowerRes to others of the same resource kind + res, ok := r.(*BmcPowerRes) + if !ok { + return fmt.Errorf("not a %s", obj.Kind()) + } + + if obj.Hostname != res.Hostname { + return fmt.Errorf("the Hostname differs") + } + if obj.Port != res.Port { + return fmt.Errorf("the Port differs") + } + if obj.Username != res.Username { + return fmt.Errorf("the Username differs") + } + if obj.Password != res.Password { + return fmt.Errorf("the Password differs") + } + if obj.InsecurePassword != res.InsecurePassword { + return fmt.Errorf("the InsecurePassword differs") + } + + if obj.Driver != res.Driver { + return fmt.Errorf("the Driver differs") + } + if obj.State != res.State { + return fmt.Errorf("the State differs") + } + + return nil +} + +// UnmarshalYAML is the custom unmarshal handler for this struct. It is +// primarily useful for setting the defaults. +func (obj *BmcPowerRes) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawRes BmcPowerRes // indirection to avoid infinite recursion + + def := obj.Default() // get the default + res, ok := def.(*BmcPowerRes) // put in the right format + if !ok { + return fmt.Errorf("could not convert to BmcPowerRes") + } + raw := rawRes(*res) // convert; the defaults go here + + if err := unmarshal(&raw); err != nil { + return err + } + + *obj = BmcPowerRes(raw) // restore from indirection with type conversion! + return nil +} diff --git a/examples/lang/bmc-power.mcl b/examples/lang/bmc-power.mcl new file mode 100644 index 0000000000..187e378dc8 --- /dev/null +++ b/examples/lang/bmc-power.mcl @@ -0,0 +1,12 @@ +# A standard bmc might connect with: +bmc:power "gofishs://ADMIN@127.0.0.1:8800" { + #username => "ADMIN", + password => "ADMIN", + #driver => "gofishs", # https gofish + + state => "on", + + Meta:poll => 10, # required until BMC's support real events! +} + +# The testing rpc example can connect to: rpcs://127.0.0.1:8800 instead. diff --git a/examples/mockbmc/fixtures/service_root.json b/examples/mockbmc/fixtures/service_root.json new file mode 100644 index 0000000000..a88dde2de5 --- /dev/null +++ b/examples/mockbmc/fixtures/service_root.json @@ -0,0 +1,45 @@ +{ + "@odata.context": "/redfish/v1/$metadata#ServiceRoot.ServiceRoot", + "@odata.type": "#ServiceRoot.v1_1_0.ServiceRoot", + "@odata.id": "/redfish/v1/", + "Id": "RootService", + "Name": "Root Service", + "RedfishVersion": "1.0.1", + "UUID": "00000000-0000-0000-0000-0CC47A8BDADA", + "Systems": { + "@odata.id": "/redfish/v1/Systems" + }, + "Chassis": { + "@odata.id": "/redfish/v1/Chassis" + }, + "Managers": { + "@odata.id": "/redfish/v1/Managers" + }, + "Tasks": { + "@odata.id": "/redfish/v1/TaskService" + }, + "SessionService": { + "@odata.id": "/redfish/v1/SessionService" + }, + "AccountService": { + "@odata.id": "/redfish/v1/AccountService" + }, + "EventService": { + "@odata.id": "/redfish/v1/EventService" + }, + "UpdateService": { + "@odata.id": "/redfish/v1/UpdateService" + }, + "Registries": { + "@odata.id": "/redfish/v1/Registries" + }, + "JsonSchemas": { + "@odata.id": "/redfish/v1/JsonSchemas" + }, + "Links": { + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions" + } + }, + "Oem": {} +} diff --git a/examples/mockbmc/fixtures/session_delete.json b/examples/mockbmc/fixtures/session_delete.json new file mode 100644 index 0000000000..e7f266f86a --- /dev/null +++ b/examples/mockbmc/fixtures/session_delete.json @@ -0,0 +1,6 @@ +{ + "Success": { + "code": "Base.v1_4_0.Success", + "Message": "Successfully Completed Request." + } +} diff --git a/examples/mockbmc/fixtures/session_service.json b/examples/mockbmc/fixtures/session_service.json new file mode 100644 index 0000000000..3ac2889fba --- /dev/null +++ b/examples/mockbmc/fixtures/session_service.json @@ -0,0 +1,10 @@ +{ + "@odata.context": "/redfish/v1/$metadata#Session.Session", + "@odata.type": "#Session.v1_0_0.Session", + "@odata.id": "/redfish/v1/SessionService/Sessions/1", + "Id": "1", + "Name": "User Session", + "Description": "Manager User Session", + "UserName": "ADMIN", + "Oem": {} +} diff --git a/examples/mockbmc/fixtures/systems.json b/examples/mockbmc/fixtures/systems.json new file mode 100644 index 0000000000..306f2545c4 --- /dev/null +++ b/examples/mockbmc/fixtures/systems.json @@ -0,0 +1,13 @@ +{ + "@odata.context": "/redfish/v1/$metadata#ComputerSystemCollection.ComputerSystemCollection", + "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection", + "@odata.id": "/redfish/v1/Systems", + "Name": "Computer System Collection", + "Description": "Computer System Collection", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/1" + } + ] +} diff --git a/examples/mockbmc/fixtures/systems_1.json.tmpl b/examples/mockbmc/fixtures/systems_1.json.tmpl new file mode 100644 index 0000000000..0152a8f242 --- /dev/null +++ b/examples/mockbmc/fixtures/systems_1.json.tmpl @@ -0,0 +1,107 @@ +{ + "@odata.context": "/redfish/v1/$metadata#ComputerSystem.ComputerSystem", + "@odata.type": "#ComputerSystem.v1_3_0.ComputerSystem", + "@odata.id": "/redfish/v1/Systems/1", + "Id": "1", + "Name": "System", + "Description": "Description of server", + "Status": { + "State": "Enabled", + "Health": "Critical" + }, + "SerialNumber": " ", + "PartNumber": "", + "SystemType": "Physical", + "BiosVersion": "3.3", + "Manufacturer": "Supermicro", + "Model": "Super Server", + "SKU": "To be filled by O.E.M.", + "UUID": "00000000-0000-0000-0000-0CC47A847624", + "ProcessorSummary": { + "Count": 1, + "Model": "Intel(R) Xeon(R) processor", + "Status": { + "State": "Enabled", + "Health": "OK" + } + }, + "MemorySummary": { + "TotalSystemMemoryGiB": 16, + "Status": { + "State": "Enabled", + "Health": "OK" + } + }, + "IndicatorLED": "Off", + "PowerState": "{{ .PowerState }}", + "Boot": { + "BootSourceOverrideEnabled": "Disabled", + "BootSourceOverrideTarget": "None", + "BootSourceOverrideTarget@Redfish.AllowableValues": [ + "None", + "Pxe", + "Hdd", + "Diags", + "CD/DVD", + "BiosSetup", + "FloppyRemovableMedia", + "UsbKey", + "UsbHdd", + "UsbFloppy", + "UsbCd", + "UefiUsbKey", + "UefiCd", + "UefiHdd", + "UefiUsbHdd", + "UefiUsbCd" + ] + }, + "Processors": { + "@odata.id": "/redfish/v1/Systems/1/Processors" + }, + "Memory": { + "@odata.id": "/redfish/v1/Systems/1/Memory" + }, + "EthernetInterfaces": { + "@odata.id": "/redfish/v1/Systems/1/EthernetInterfaces" + }, + "SimpleStorage": { + "@odata.id": "/redfish/v1/Systems/1/SimpleStorage" + }, + "Storage": { + "@odata.id": "/redfish/v1/Systems/1/Storage" + }, + "LogServices": { + "@odata.id": "/redfish/v1/Systems/1/LogServices" + }, + "Links": { + "Chassis": [ + { + "@odata.id": "/redfish/v1/Chassis/1" + } + ], + "ManagedBy": [ + { + "@odata.id": "/redfish/v1/Managers/1" + } + ], + "Oem": {} + }, + "Actions": { + "#ComputerSystem.Reset": { + "target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", + "ResetType@Redfish.AllowableValues": [ + "On", + "ForceOff", + "GracefulShutdown", + "GracefulRestart", + "ForceRestart", + "Nmi", + "ForceOn" + ] + } + }, + "Oem": { + "Supermicro": {} + } +} diff --git a/examples/mockbmc/mockbmc.go b/examples/mockbmc/mockbmc.go new file mode 100644 index 0000000000..036d54ed12 --- /dev/null +++ b/examples/mockbmc/mockbmc.go @@ -0,0 +1,297 @@ +// This is an example mock BMC server/device. +// Many thanks to Joel Rebello for figuring out the specific endpoints needed. +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "text/template" + + "github.com/purpleidea/mgmt/util" + + "github.com/alexflint/go-arg" + "github.com/bmc-toolbox/bmclib/v2/providers/rpc" +) + +const ( + // DefaultPort to listen on. Seen in the bmclib docs. + DefaultPort = 8800 + + // StateOn is the power "on" state. + StateOn = "on" + + // StateOff is the power "off" state. + StateOff = "off" +) + +// MockBMC is a simple mocked BMC device. +type MockBMC struct { + // Addr to listen on. Eg: :8800 for example. + Addr string + + // State of the device power. This gets read and changed by the API. + State string + + // Driver specifies which driver we want to mock. + // TODO: Do I mean "driver" or "provider" ? + Driver string +} + +// Data is what we use to template the outputs. +type Data struct { + PowerState string +} + +// Run kicks this all off. +func (obj *MockBMC) Run() error { + + tls := util.NewTLS() + tls.Host = "localhost" // TODO: choose something + keyPemFile := "/tmp/key.pem" + certPemFile := "/tmp/cert.pem" + + if err := tls.Generate(keyPemFile, certPemFile); err != nil { + return err + } + + fmt.Printf("running at: %s\n", obj.Addr) + fmt.Printf("driver is: %s\n", obj.Driver) + fmt.Printf("device is: %s\n", obj.State) // we start off in this state + if obj.Driver == "rpc" { + http.HandleFunc("/", obj.rpcHandler) + } + if obj.Driver == "redfish" || obj.Driver == "gofish" { + http.HandleFunc("/redfish/v1/", obj.endpointFunc("service_root.json", http.MethodGet, 200, nil)) + + // login + sessionHeader := map[string]string{ + "X-Auth-Token": "t5tpiajo89fyvvel5434h9p2l3j69hzx", // TODO: how do we get this? + "Location": "/redfish/v1/SessionService/Sessions/1", + } + + http.HandleFunc("/redfish/v1/SessionService/Sessions", obj.endpointFunc("session_service.json", http.MethodPost, 201, sessionHeader)) + + // get power state + http.HandleFunc("/redfish/v1/Systems", obj.endpointFunc("systems.json", http.MethodGet, 200, nil)) + http.HandleFunc("/redfish/v1/Systems/1", obj.endpointFunc("systems_1.json.tmpl", http.MethodGet, 200, nil)) + + // set pxe - we can't have two routes with the same pattern + //http.HandleFunc("/redfish/v1/Systems/1", obj.endpointFunc("", http.MethodPatch, 200, nil)) + + // power on/off XXX: seems to post here to turn on + http.HandleFunc("/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", obj.endpointFunc("", http.MethodPost, 200, nil)) + + // logoff + http.HandleFunc("/redfish/v1/SessionService/Sessions/1", obj.endpointFunc("session_delete.json", http.MethodDelete, 200, nil)) + } + + http.HandleFunc("/hello", obj.hello) + //return http.ListenAndServe(obj.Addr, nil) + return http.ListenAndServeTLS(obj.Addr, certPemFile, keyPemFile, nil) +} + +func (obj *MockBMC) template(templateText string, data interface{}) (string, error) { + var err error + tmpl := template.New("name") // whatever name you want + //tmpl = tmpl.Funcs(funcMap) + tmpl, err = tmpl.Parse(templateText) + if err != nil { + return "", err + } + + buf := new(bytes.Buffer) + + // run the template + if err := tmpl.Execute(buf, data); err != nil { + return "", err + } + return buf.String(), nil +} + +// endpointFunc handles the bmc mock requirements. +func (obj *MockBMC) endpointFunc(file, method string, retStatus int, retHeader map[string]string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + data := &Data{ + PowerState: obj.State, + } + + fmt.Printf("URL: %s\n", r.URL.Path) + fmt.Printf("[%s] file: %s\n", method, file) + //if method == "POST" { + //for name, values := range r.Header { + // fmt.Printf("\t[%s] header: %+v\n", name, values) + //} + + if err := r.ParseForm(); err != nil { + fmt.Printf("error parsing form: %+v\n", err) + } + for name, values := range r.PostForm { + fmt.Printf("\t[%s] values: %+v\n", name, values) + } + //} + + // purge check on patch method if set pxe request is attempted + if r.Method != method && r.Method != http.MethodPatch { + resp := fmt.Sprintf("unexpected request - url: %s, method: %s", r.URL, r.Method) + _, _ = w.Write([]byte(resp)) + } + + for k, v := range retHeader { + w.Header().Add(k, v) + } + + w.WriteHeader(retStatus) + if file != "" { + out1 := mustReadFile(file) + if !strings.HasSuffix(file, ".tmpl") { + _, _ = w.Write(out1) + return + } + + out2, err := obj.template(string(out1), data) + if err != nil { + resp := fmt.Sprintf("unexpected request - url: %s, method: %s", r.URL, r.Method) + _, _ = w.Write([]byte(resp)) + return + } + _, _ = w.Write([]byte(out2)) + + } + + return + } +} + +// rpcHandler is used for the rpc driver. +func (obj *MockBMC) rpcHandler(w http.ResponseWriter, r *http.Request) { + //fmt.Printf("req1: %+v\n", r) + //fmt.Printf("method: %s\n", r.Method) + //fmt.Printf("URL: %s\n", r.URL) + //fmt.Printf("Body: %v\n", r.Body) + + req := rpc.RequestPayload{} + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + //fmt.Printf("data: %+v\n", req) + + rp := rpc.ResponsePayload{ + ID: req.ID, + Host: req.Host, + } + switch req.Method { + case rpc.PowerGetMethod: + rp.Result = obj.State + fmt.Printf("get state: %s\n", obj.State) + + case rpc.PowerSetMethod: + //fmt.Printf("req2: %T %+v\n", req.Params, req.Params) + // TODO: This is a mess, isn't there a cleaner way to unpack it? + m, ok := req.Params.(map[string]interface{}) + if ok { + param, exists := m["state"] + state, ok := param.(string) + if ok { + if exists && (state == StateOn || state == StateOff) { + obj.State = state + fmt.Printf("set state: %s\n", state) + } + } + } + + case rpc.BootDeviceMethod: + + case rpc.PingMethod: + fmt.Printf("got ping\n") + rp.Result = "pong" + + default: + w.WriteHeader(http.StatusNotFound) + } + b, _ := json.Marshal(rp) + //fmt.Printf("out: %s\n", b) + w.Write(b) +} + +func (obj *MockBMC) hello(w http.ResponseWriter, req *http.Request) { + fmt.Printf("req: %+v\n", req) + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte("This is hello world!\n")) + //w.Write([]byte("OpenBMC says hello!\n")) +} + +func mustReadFile(filename string) []byte { + fixture := "fixtures" + "/" + filename + fh, err := os.Open(fixture) + if err != nil { + panic(err) + } + + defer fh.Close() + + b, err := io.ReadAll(fh) + if err != nil { + panic(err) + } + + // The original examples had no trailing newlines. Not sure if allowed. + return bytes.TrimSuffix(b, []byte("\n")) +} + +// Args are what are used to build the CLI. +type Args struct { + // XXX: We cannot have both subcommands and a positional argument. + // XXX: I think it's a bug of this library that it can't handle argv[0]. + //Argv0 string `arg:"positional"` + + On bool `arg:"--on" help:"start on"` + + Port int `arg:"--port" help:"port to listen on"` + + Driver string `arg:"--driver" default:"redfish" help:"driver to use"` +} + +// Main program that returns error. +func Main() error { + args := Args{ + Port: DefaultPort, + } + config := arg.Config{} + parser, err := arg.NewParser(config, &args) + if err != nil { + // programming error + return err + } + err = parser.Parse(os.Args[1:]) // XXX: args[0] needs to be dropped + if err == arg.ErrHelp { + parser.WriteHelp(os.Stdout) + return nil + } + + state := StateOff + if args.On { + state = StateOn + } + + mock := &MockBMC{ + Addr: fmt.Sprintf("localhost:%d", args.Port), + //State: StateOff, // starts off off + //State: StateOn, + State: state, + Driver: args.Driver, + } + return mock.Run() +} + +// wget --no-check-certificate --post-data 'user=foo&password=bar' \ +// https://localhost:8800/redfish/v1/Systems/1/Actions/ComputerSystem.Reset -O - +func main() { + fmt.Printf("main: %+v\n", Main()) +} diff --git a/go.mod b/go.mod index 2a14d59038..d1392af8f4 100644 --- a/go.mod +++ b/go.mod @@ -38,8 +38,8 @@ require ( go.etcd.io/etcd/client/v3 v3.5.13 go.etcd.io/etcd/server/v3 v3.5.13 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.22.0 - golang.org/x/sys v0.19.0 + golang.org/x/crypto v0.23.0 + golang.org/x/sys v0.20.0 golang.org/x/time v0.5.0 golang.org/x/tools v0.20.0 gopkg.in/yaml.v2 v2.4.0 @@ -48,13 +48,18 @@ require ( require ( dario.cat/mergo v1.0.0 // indirect + github.com/Jeffail/gabs/v2 v2.7.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230 // indirect + github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22 // indirect github.com/alexflint/go-scalar v1.2.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/ashcrow/osrelease v0.0.0-20180626175927-9b292693c55c // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.13.0 // indirect + github.com/bmc-toolbox/bmclib/v2 v2.3.3 // indirect + github.com/bmc-toolbox/common v0.0.0-20240806132831-ba8adc6a35e3 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb // indirect @@ -67,6 +72,7 @@ require ( github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-logr/logr v1.4.1 // indirect @@ -90,6 +96,8 @@ require ( github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/serf v0.10.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jacobweinstock/iamt v0.0.0-20230502042727-d7cdbe67d9ef // indirect + github.com/jacobweinstock/registrar v0.4.7 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect @@ -121,12 +129,14 @@ require ( github.com/prometheus/common v0.53.0 // indirect github.com/prometheus/procfs v0.14.0 // indirect github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect + github.com/satori/go.uuid v1.2.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace // indirect + github.com/stmcginnis/gofish v0.19.0 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect github.com/tredoe/osutil v1.5.0 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect @@ -150,10 +160,10 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/term v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect google.golang.org/genproto v0.0.0-20240415180920-8c6c420018be // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect diff --git a/go.sum b/go.sum index 22acfd9986..9dd3adea59 100644 --- a/go.sum +++ b/go.sum @@ -6,11 +6,17 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Jeffail/gabs/v2 v2.7.0 h1:Y2edYaTcE8ZpRsR2AtmPu5xQdFDIthFG0jYhu5PY8kg= +github.com/Jeffail/gabs/v2 v2.7.0/go.mod h1:dp5ocw1FvBBQYssgHsG7I1WYsiLRtkUaB1FEtSwvNUw= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230 h1:t95Grn2mOPfb3+kPDWsNnj4dlNcxnvuR72IjY8eYjfQ= +github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230/go.mod h1:t2EzW1qybnPDQ3LR/GgeF0GOzHUXT5IVMLP2gkW1cmc= +github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22 h1:a0MBqYm44o0NcthLKCljZHe1mxlN6oahCQHHThnSwB4= +github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22/go.mod h1:/B7V22rcz4860iDqstGvia/2+IYWXf3/JdQCVd/1D2A= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -43,6 +49,10 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bmc-toolbox/bmclib/v2 v2.3.3 h1:ORCB91SrCShUnJ4vwQ00OPSmNg+08NLgaBH/ezfMKjQ= +github.com/bmc-toolbox/bmclib/v2 v2.3.3/go.mod h1:t8If/0fHQTRIK/yKDk2H3SgthDNNj+7z2aeftDFRFrU= +github.com/bmc-toolbox/common v0.0.0-20240806132831-ba8adc6a35e3 h1:/BjZSX/sphptIdxpYo4wxAQkgMLyMMgfdl48J9DKNeE= +github.com/bmc-toolbox/common v0.0.0-20240806132831-ba8adc6a35e3/go.mod h1:Cdnkm+edb6C0pVkyCrwh3JTXAe0iUF9diDG/DztPI9I= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= @@ -108,6 +118,7 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= @@ -236,6 +247,10 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c h1:OCFM4+DXTWfNlyeoddrTwdup/ztkGSyAMR2UGcPckNQ= github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= +github.com/jacobweinstock/iamt v0.0.0-20230502042727-d7cdbe67d9ef h1:G4k02HGmBUfJFSNu3gfKJ+ki+B3qutKsYzYndkqqKc4= +github.com/jacobweinstock/iamt v0.0.0-20230502042727-d7cdbe67d9ef/go.mod h1:FgmiLTU6cJewV4Xgrq6m5o8CUlTQOJtqzaFLGA0mG+E= +github.com/jacobweinstock/registrar v0.4.7 h1:s4dOExccgD+Pc7rJC+f3Mc3D+NXHcXUaOibtcEsPxOc= +github.com/jacobweinstock/registrar v0.4.7/go.mod h1:PWmkdGFG5/ZdCqgMo7pvB3pXABOLHc5l8oQ0sgmBNDU= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -397,6 +412,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= @@ -421,6 +438,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace h1:9PNP1jnUjRhfmGMlkXHjYPishpcw4jpSt/V/xYY3FMA= github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stmcginnis/gofish v0.19.0 h1:fmxdRZ5WHfs+4ExArMYoeRfoh+SAxLELKtmoVplBkU4= +github.com/stmcginnis/gofish v0.19.0/go.mod h1:lq2jHj2t8Krg0Gx02ABk8MbK7Dz9jvWpO/TGnVksn00= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= @@ -517,6 +536,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= @@ -560,6 +581,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -626,6 +649,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -633,6 +658,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -643,6 +670,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/lang/core/embedded/provisioner/main.mcl b/lang/core/embedded/provisioner/main.mcl index ce261b6cc3..8e0d627956 100644 --- a/lang/core/embedded/provisioner/main.mcl +++ b/lang/core/embedded/provisioner/main.mcl @@ -560,6 +560,9 @@ class base:host($name, $config) { $handoff_code = $config->handoff_code || "" panic(not strings.has_prefix($handoff_code, "/")) + # This is a giant driver://user:password@host:port/whatever URL... + $bmc_uri = $config->bmc_uri || "" + # unique host key which is usually a mac address unless it's a default $hkey = if $mac == "" { "default" @@ -819,6 +822,15 @@ class base:host($name, $config) { Before => Print["ready"], } + bmc:power "${bmc_uri}" { # TODO: Name() API is not yet stable + #password => "hunter2", + insecure_password => true, # XXX: get from uri for now + + state => "on", + + Meta:poll => 60, # required until BMC's support real events! + } + ##$str_true = convert.format_bool(true) ##$str_false = convert.format_bool(false) #http:flag "${name}" { diff --git a/lang/core/embedded/provisioner/provisioner.go b/lang/core/embedded/provisioner/provisioner.go index 56d2e30f1c..8499d16330 100644 --- a/lang/core/embedded/provisioner/provisioner.go +++ b/lang/core/embedded/provisioner/provisioner.go @@ -171,6 +171,10 @@ type localArgs struct { // static code deploy bolus. This is useful for isolated, one-time runs. HandoffCode string `arg:"--handoff-code" help:"code dir to handoff to host" func:"cli_handoff_code"` // eg: /etc/mgmt/ + // BmcURI specifies the BMC connect string we want to use for this host. + // This is a giant driver://user:password@host:port/whatever URL... + BmcURI string `arg:"--bmc-uri" help:"bmc connect string to use for this host" func:"cli_bmc_uri"` + // OnlyUnify tells the compiler to stop after type unification. This is // used for testing. OnlyUnify bool `arg:"--only-unify" help:"stop after type unification"` diff --git a/lang/core/embedded/provisioner/top.mcl b/lang/core/embedded/provisioner/top.mcl index f985eab16d..0ccca9087c 100644 --- a/lang/core/embedded/provisioner/top.mcl +++ b/lang/core/embedded/provisioner/top.mcl @@ -79,6 +79,7 @@ include base.host("host0", struct{ # TODO: do we need a usable name anywhere? handoff => $handoff, # alternatively some code word or querystring #handoff_code => "/etc/mgmt/", # one way to do it handoff_code => provisioner.cli_handoff_code(), + bmc_uri => provisioner.cli_bmc_uri(), }) as host0 #if $host0.provisioned {