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