279 lines
8.4 KiB
Go
279 lines
8.4 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": map[string]any{
|
|
"name": "David J. Allen",
|
|
"email": "davidallendj@gmail.com",
|
|
"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(store storage.KVStore, args []string) error {
|
|
// set all the defaults for variables
|
|
var (
|
|
client SmdClient
|
|
bytes []byte
|
|
err error
|
|
)
|
|
|
|
log.Debug().
|
|
Str("plugin", p.Name()).
|
|
Strs("args", args).
|
|
Int("arg_count", len(args)).
|
|
Any("store", store).
|
|
Msg("(smd) Run()")
|
|
|
|
// 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")
|
|
}
|
|
store.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
|