Instead of constantly making these updates, let's just remove the year since things are stored in git anyways, and this is not an actual modern legal risk anymore.
467 lines
13 KiB
Go
467 lines
13 KiB
Go
// Mgmt
|
|
// Copyright (C) James Shubin and the project contributors
|
|
// Written by James Shubin <james@shubin.ca> 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 <https://www.gnu.org/licenses/>.
|
|
//
|
|
// 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
|
|
}
|