Skip to content

Instantly share code, notes, and snippets.

@joelrebel
Created October 30, 2024 21:21
Show Gist options
  • Save joelrebel/5fe3201e85303b3f8b49ad35e16819aa to your computer and use it in GitHub Desktop.
Save joelrebel/5fe3201e85303b3f8b49ad35e16819aa to your computer and use it in GitHub Desktop.
mgmt server power on/off
```
Mock bmc with https://github.com/joelrebel/mockbmc
```
```sh
GOTAGS="noaugeas novirt nodocker" make
./mgmt run --tmp-prefix lang examples/lang/serverpower.mcl
# To dump outgoing bmclib requests set `DEBUG_BMCLIB=true`
```
```examples/lang/serverpower.mcl
server:power "power-state-on" {
hostname => "localhost",
port => 8800,
username => "ADMIN",
password => "ADMIN",
state => "on",
}
```
```engine/resources/serverctl.go
// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Written by James Shubin <[email protected]> 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"
"strconv"
"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/openbmc"
)
func init() {
engine.RegisterResource("server:power", func() engine.Res { return &ServerRes{} })
}
const (
// DefaultBmcPowerPort is the default port we try to connect on.
DefaultBmcPowerPort = 443
)
// ServerRes is a no-op resource that does nothing.
type ServerRes struct {
traits.Base // add the base methods without re-implementation
init *engine.Init
// Hostname to connect to.
Hostname string `lang:"hostname" yaml:"hostname"`
// Port to connect to.
Port int `lang:"port" yaml:"port"`
// Username to use to connect.
Username string `lang:"username" yaml:"username"`
// Password to use to connect.
// XXX: Use mgmt magic credentials in the future.
Password string `lang:"password" yaml:"password"`
// Driver to use, such as: "gofish". If you set this, then
// PreferredDriver is ignored.
Driver string `lang:"driver" yaml:"driver"`
// PreferredDriver to use, such as: "gofish".
// TODO: Should we Validate that Driver is not also specified?
PreferredDriver string `lang:"preferred_driver" yaml:"preferred_driver"`
// State of machine power. Can be "on" or "off".
State string `lang:"state" yaml:"state"`
}
// 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 *ServerRes) getHostname() string {
if obj.Hostname != "" {
return obj.Hostname
}
// 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(obj.Name())
if err != nil {
return obj.Name() // 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.
func (obj *ServerRes) getPort() string {
if obj.Port != 0 {
return strconv.Itoa(obj.Port)
}
// 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(obj.Name())
if err != nil {
return strconv.Itoa(DefaultBmcPowerPort) // default port
}
_ = host
return port
}
// getUsername returns the username that we want to connect with.
// TODO: If the Username field is not set, should we parse from the Name?
func (obj *ServerRes) getUsername() string {
return obj.Username
}
func (obj *ServerRes) getPassword() string {
return obj.Password
}
// Default returns some sensible defaults for this resource.
func (obj *ServerRes) Default() engine.Res {
return &ServerRes{}
}
// Validate if the params passed in are valid data.
func (obj *ServerRes) 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")
//}
return nil
}
// Init runs some startup code for this resource.
func (obj *ServerRes) Init(init *engine.Init) error {
obj.init = init // save for later
return nil
}
// Cleanup is run by the engine to clean up after the resource is done.
func (obj *ServerRes) Cleanup() error {
return nil
}
// Watch is the primary listener for this resource and it outputs events.
func (obj *ServerRes) 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
}
//func (obj *ServerRes) connect(ctx context.Context error {
// TODO: refactor all that mess into here
//}
// CheckApply method for BmcPower resource. Does nothing, returns happy!
func (obj *ServerRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
// limit client to gofish provider
client := bmclib.NewClient(
"https://"+obj.Hostname,
obj.getUsername(),
obj.getPassword(),
bmclib.WithRedfishPort(obj.getPort()),
).For("gofish")
if err := client.Open(ctx); err != nil {
return false, err
}
defer client.Close(ctx) // (err error)
if obj.init.Debug || true { // XXX !!!
obj.init.Logf("connected ok")
}
state, err := client.GetPowerState(ctx)
if err != nil {
return false, err
}
obj.init.Logf("get state: %s", state)
if !apply {
return false, nil
}
if obj.State == state {
return true, nil
}
ok, err := client.SetPowerState(ctx, obj.State)
if err != nil {
return false, err
}
if !ok {
// TODO: When is this ever false?
// - in a very specific case, although I don't think its helpful
// https://github.com/bmc-toolbox/bmclib/blob/f669e7b6a102fddb2a8eb99484897b8e68cf0bf1/internal/redfishwrapper/power.go#L135
}
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 *ServerRes) Cmp(r engine.Res) error {
// we can only compare ServerRes to others of the same resource kind
res, ok := r.(*ServerRes)
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.Driver != res.Driver {
return fmt.Errorf("the Driver differs")
}
if obj.PreferredDriver != res.PreferredDriver {
return fmt.Errorf("the PreferredDriver 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 *ServerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes ServerRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*ServerRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to ServerRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = ServerRes(raw) // restore from indirection with type conversion!
return nil
}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment