From ea4819e97a7538c1d71db15a36368785fd80ff64 Mon Sep 17 00:00:00 2001 From: David Allen Date: Sun, 24 Aug 2025 20:34:38 -0600 Subject: [PATCH] refactor: moved plugin files --- pkg/plugins/jinja2.go | 58 ------ pkg/plugins/jinja2/jinja2.go | 98 +++++++++ pkg/plugins/smd.go | 1 - pkg/plugins/smd/smd.go | 270 +++++++++++++++++++++++++ pkg/plugins/{ => userdata}/userdata.go | 6 +- 5 files changed, 372 insertions(+), 61 deletions(-) delete mode 100644 pkg/plugins/jinja2.go create mode 100644 pkg/plugins/jinja2/jinja2.go delete mode 100644 pkg/plugins/smd.go create mode 100644 pkg/plugins/smd/smd.go rename pkg/plugins/{ => userdata}/userdata.go (87%) diff --git a/pkg/plugins/jinja2.go b/pkg/plugins/jinja2.go deleted file mode 100644 index ffbbd77..0000000 --- a/pkg/plugins/jinja2.go +++ /dev/null @@ -1,58 +0,0 @@ -package plugin - -import ( - "bytes" - - "git.towk2.me/towk/configurator/pkg/storage" - "github.com/nikolalohinski/gonja/v2" - "github.com/nikolalohinski/gonja/v2/exec" -) - -type Jinja2 struct{} - -func (p *Jinja2) Name() string { return "jinja2" } -func (p *Jinja2) Version() string { return "test" } -func (p *Jinja2) Description() string { return "Renders Jinja 2 templates" } -func (p *Jinja2) Metadata() map[string]string { - return map[string]string{ - "author.name": "David J. Allen", - "author.email": "davidallendj@gmail.com", - } -} - -func (p *Jinja2) Init() error { - // nothing to initialize - return nil -} - -func (p *Jinja2) Run(data storage.KVStore, args []string) error { - // render the files using Jinja 2 from args - newContent := []string{} - for _, arg := range args { - var ( - context *exec.Context - template *exec.Template - output bytes.Buffer - err error - ) - template, err = gonja.FromString(arg) - if err != nil { - panic(err) - } - - context = exec.NewContext(data.GetData().(map[string]any)) - if err = template.Execute(&output, context); err != nil { // Prints: Hello Bob! - panic(err) - } - newContent = append(newContent, output.String()) - } - - // write back to the data storage - data.Set("out", newContent) - return nil -} - -func (p *Jinja2) Cleanup() error { - // nothing to clean up - return nil -} diff --git a/pkg/plugins/jinja2/jinja2.go b/pkg/plugins/jinja2/jinja2.go new file mode 100644 index 0000000..4b64a11 --- /dev/null +++ b/pkg/plugins/jinja2/jinja2.go @@ -0,0 +1,98 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + + configurator "git.towk2.me/towk/makeshift/pkg" + "git.towk2.me/towk/makeshift/pkg/storage" + "github.com/nikolalohinski/gonja/v2" + "github.com/nikolalohinski/gonja/v2/exec" + "github.com/rs/zerolog/log" +) + +type Jinja2 struct{} + +func (p *Jinja2) Name() string { return "jinja2" } +func (p *Jinja2) Version() string { return "v0.0.1-alpha" } +func (p *Jinja2) Description() string { return "Renders Jinja 2 templates" } +func (p *Jinja2) Metadata() configurator.Metadata { + return configurator.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 *Jinja2) Init() error { + // nothing to initialize + log.Debug().Str("plugin", p.Name()).Msg("jinja2.Init()") + return nil +} + +func (p *Jinja2) Run(data storage.KVStore, args []string) error { + // render the files using Jinja 2 from args + var ( + rendered []string + context *exec.Context + template *exec.Template + mappings map[string]any + input any // must be a byte array + output bytes.Buffer + err error + ) + log.Debug(). + Str("plugin", p.Name()). + Any("data", data). + // Bytes("input", input.([]byte)). + Int("arg_count", len(args)). + Msg("Run()") + + input, err = data.Get("file") + if err != nil { + return fmt.Errorf("(jinja2) failed to get input data: %v", err) + } + + // get the templates provided as args to the plugin + template, err = gonja.FromBytes(input.([]byte)) + if err != nil { + return fmt.Errorf("(jinja2) failed to get template from args: %v", err) + } + + // get mappings from shared data + shared, err := data.Get("shared") + if err != nil { + return fmt.Errorf("(jinja2) failed to get data from store: %v", err) + } + + err = json.Unmarshal(shared.([]byte), &mappings) + if err != nil { + return fmt.Errorf("(jinja2) failed to unmarshal mappings from shared data: %v", err) + } + + data.Set("mappings", mappings) + + // use the provided data in the store to render templates + // NOTE: this may be changed to specifically use "shared" data instead + context = exec.NewContext(data.GetData().(map[string]any)) + if err = template.Execute(&output, context); err != nil { // Prints: Hello Bob! + return fmt.Errorf("(jinja2) failed to render template: %v", err) + } + rendered = append(rendered, output.String()) + + // write render templates to data store output + data.Set("out", rendered) + return nil +} + +func (p *Jinja2) Cleanup() error { + // nothing to clean up + log.Debug().Str("plugin", p.Name()).Msg("jinja2.Cleanup()") + return nil +} + +var Makeshift Jinja2 diff --git a/pkg/plugins/smd.go b/pkg/plugins/smd.go deleted file mode 100644 index b0736c3..0000000 --- a/pkg/plugins/smd.go +++ /dev/null @@ -1 +0,0 @@ -package plugin diff --git a/pkg/plugins/smd/smd.go b/pkg/plugins/smd/smd.go new file mode 100644 index 0000000..cb263ab --- /dev/null +++ b/pkg/plugins/smd/smd.go @@ -0,0 +1,270 @@ +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 diff --git a/pkg/plugins/userdata.go b/pkg/plugins/userdata/userdata.go similarity index 87% rename from pkg/plugins/userdata.go rename to pkg/plugins/userdata/userdata.go index 4b274b9..ae5d31f 100644 --- a/pkg/plugins/userdata.go +++ b/pkg/plugins/userdata/userdata.go @@ -1,6 +1,6 @@ -package plugin +package main -import "git.towk2.me/towk/configurator/pkg/storage" +import "git.towk2.me/towk/makeshift/pkg/storage" type UserData struct{} @@ -26,3 +26,5 @@ func (p *UserData) Run(data storage.KVStore, args []string) error { func (p *UserData) Clean() error { return nil } + +var Makeshift UserData