makeshift/pkg/plugins/smd/smd.go

270 lines
8.2 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
makeshift "git.towk2.me/towk/makeshift/pkg"
"git.towk2.me/towk/makeshift/pkg/storage"
"github.com/rs/zerolog/log"
)
// An struct that's meant to extend functionality of the base HTTP client by
// adding commonly made requests to SMD. The implemented functions are can be
// used in generator plugins to fetch data when it is needed to substitute
// values for the Jinja templates used.
type SmdClient struct {
http.Client `json:"-" yaml:"-"`
Host string `yaml:"host"`
Port int `yaml:"port"`
AccessToken string `yaml:"access-token"`
RedfishEndpoints []RedfishEndpoint `json:"redfish_endpoints"`
EthernetInterfaces []EthernetInterface `json:"ethernet_interfaces"`
Components []Component `json:"components"`
}
type IPAddr struct {
IpAddress string `json:"IPAddress"`
Network string `json:"Network"`
}
type EthernetInterface struct {
ID string `json:"ID"`
Description string `json:"Description"`
MacAddress string `json:"MACAddr"`
LastUpdate string `json:"LastUpdate"`
ComponentID string `json:"ComponentID"`
Type string `json:"Type"`
IpAddresses []IPAddr `json:"IPAddresses"`
}
type Component struct {
ID string `json:"ID"`
Type string `json:"Type"`
State string `json:"State,omitempty"`
Flag string `json:"Flag,omitempty"`
Enabled *bool `json:"Enabled,omitempty"`
SwStatus string `json:"SoftwareStatus,omitempty"`
Role string `json:"Role,omitempty"`
SubRole string `json:"SubRole,omitempty"`
NID json.Number `json:"NID,omitempty"`
Subtype string `json:"Subtype,omitempty"`
NetType string `json:"NetType,omitempty"`
Arch string `json:"Arch,omitempty"`
Class string `json:"Class,omitempty"`
ReservationDisabled bool `json:"ReservationDisabled,omitempty"`
Locked bool `json:"Locked,omitempty"`
}
type RedfishEndpoint struct {
ID string `json:"ID"`
Type string `json:"Type"`
Name string `json:"Name,omitempty"` // user supplied descriptive name
Hostname string `json:"Hostname"`
Domain string `json:"Domain"`
FQDN string `json:"FQDN"`
Enabled bool `json:"Enabled"`
UUID string `json:"UUID,omitempty"`
User string `json:"User"`
Password string `json:"Password"` // Temporary until more secure method
UseSSDP bool `json:"UseSSDP,omitempty"`
MACRequired bool `json:"MACRequired,omitempty"`
MACAddr string `json:"MACAddr,omitempty"`
IPAddr string `json:"IPAddress,omitempty"`
}
func (p *SmdClient) Name() string { return "smd" }
func (p *SmdClient) Version() string { return "v0.0.1-alpha" }
func (p *SmdClient) Description() string { return "Fetchs data from SMD and writes to store" }
func (p *SmdClient) Metadata() makeshift.Metadata {
return makeshift.Metadata{
"author.name": "David J. Allen",
"author.email": "davidallendj@gmail.com",
"author.links": []string{
"https://github.com/davidallendj",
"https://git.towk2.me/towk",
},
}
}
func (p *SmdClient) Init() error {
log.Debug().Str("plugin", p.Name()).Msg("smd.Init()")
return nil
}
func (p *SmdClient) Run(data storage.KVStore, args []string) error {
// set all the defaults for variables
var (
client SmdClient
bytes []byte
err error
)
// if we have a client, try making the request for the ethernet interfaces
err = client.FetchEthernetInterfaces()
if err != nil {
return fmt.Errorf("(smd) failed to fetch ethernet interfaces with client: %v", err)
}
err = client.FetchRedfishEndpoints()
if err != nil {
return fmt.Errorf("(smd) failed to fetch redfish endpoints with client: %v", err)
}
err = client.FetchComponents()
if err != nil {
return fmt.Errorf("(smd) failed to fetch components with client: %v", err)
}
// write data back to shared data store to be used by other plugins
bytes, err = json.Marshal(client)
if err != nil {
return fmt.Errorf("(smd) failed to marshal SMD client: %v")
}
data.Set("shared", bytes)
// apply template substitutions and return output as byte array
return nil
}
func (p *SmdClient) Cleanup() error {
log.Debug().Str("plugin", p.Name()).Msg("smd.Init()")
return nil
}
// Fetch the ethernet interfaces from SMD service using its API. An access token may be required if the SMD
// service SMD_JWKS_URL envirnoment variable is set.
func (client *SmdClient) FetchEthernetInterfaces() error {
var (
bytes []byte
err error
)
// make request to SMD endpoint
bytes, err = client.makeRequest("/Inventory/EthernetInterfaces")
if err != nil {
return fmt.Errorf("failed to read HTTP response: %v", err)
}
// unmarshal response body JSON and extract in object
err = json.Unmarshal(bytes, &client.EthernetInterfaces)
if err != nil {
return fmt.Errorf("failed to unmarshal response: %v", err)
}
// print what we got if verbose is set
log.Debug().Str("ethernet_interfaces", string(bytes)).Msg("found interfaces")
return nil
}
// Fetch the components from SMD using its API. An access token may be required if the SMD
// service SMD_JWKS_URL envirnoment variable is set.
func (client *SmdClient) FetchComponents() error {
var (
bytes []byte
err error
)
// make request to SMD endpoint
bytes, err = client.makeRequest("/State/Components")
if err != nil {
return fmt.Errorf("failed to make HTTP request: %v", err)
}
// make sure our response is actually JSON first
if !json.Valid(bytes) {
return fmt.Errorf("expected valid JSON response: %v", string(bytes))
}
// unmarshal response body JSON and extract in object
var tmp map[string]any
err = json.Unmarshal(bytes, &tmp)
if err != nil {
return fmt.Errorf("failed to unmarshal response: %v", err)
}
bytes, err = json.Marshal(tmp["RedfishEndpoints"].([]any))
if err != nil {
return fmt.Errorf("failed to marshal JSON: %v", err)
}
err = json.Unmarshal(bytes, &client.Components)
if err != nil {
return fmt.Errorf("failed to unmarshal response: %v", err)
}
// print what we got if verbose is set
log.Debug().Str("components", string(bytes)).Msg("found components")
return nil
}
// TODO: improve implementation of this function
func (client *SmdClient) FetchRedfishEndpoints() error {
var (
store map[string]any
rfeps []RedfishEndpoint
body []byte
err error
)
// make initial request to get JSON with 'RedfishEndpoints' as property
body, err = client.makeRequest("/Inventory/RedfishEndpoints")
if err != nil {
return fmt.Errorf("failed to make HTTP resquest: %v", err)
}
// make sure response is in JSON
if !json.Valid(body) {
return fmt.Errorf("expected valid JSON response: %s", string(body))
}
err = json.Unmarshal(body, &store)
if err != nil {
return fmt.Errorf("failed to unmarshal response: %v", err)
}
// marshal RedfishEndpoint JSON back to makeshift.RedfishEndpoint
body, err = json.Marshal(store["RedfishEndpoints"].([]any))
if err != nil {
return fmt.Errorf("failed to marshal JSON: %v", err)
}
err = json.Unmarshal(body, &rfeps)
if err != nil {
return fmt.Errorf("failed to unmarshal response: %v", err)
}
// show the final result
log.Debug().Bytes("redfish_endpoints", body).Msg("found redfish endpoints")
client.RedfishEndpoints = rfeps
return nil
}
func (client *SmdClient) makeRequest(endpoint string) ([]byte, error) {
if client == nil {
return nil, fmt.Errorf("client is nil")
}
// fetch DHCP related information from SMD's endpoint:
url := fmt.Sprintf("%s/hsm/v2%s", client.Host, endpoint)
req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer([]byte{}))
if err != nil {
return nil, fmt.Errorf("failed to create new HTTP request: %v", err)
}
// include access token in authorzation header if found
// NOTE: This shouldn't be needed for this endpoint since it's public
if client.AccessToken != "" {
req.Header.Add("Authorization", "Bearer "+client.AccessToken)
}
// make the request to SMD
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %v", err)
}
// read the contents of the response body
return io.ReadAll(res.Body)
}
var Makeshift SmdClient