Fixed server implementation and refactored

This commit is contained in:
David Allen 2024-06-20 17:09:02 -06:00
parent 0e3eec733b
commit 22195fa00a
No known key found for this signature in database
GPG key ID: 717C593FF60A2ACC
8 changed files with 289 additions and 215 deletions

View file

@ -11,7 +11,7 @@ import (
)
type SmdClient struct {
http.Client
http.Client `json:"-"`
Host string `yaml:"host"`
Port int `yaml:"port"`
AccessToken string `yaml:"access-token"`
@ -20,7 +20,7 @@ type SmdClient struct {
type Params = map[string]any
type Option func(Params)
func WithVerbose() Option {
func WithVerbosity() Option {
return func(p util.Params) {
p["verbose"] = true
}
@ -35,6 +35,11 @@ func NewParams() Params {
// 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(opts ...util.Option) ([]EthernetInterface, error) {
var (
params = util.GetParams(opts...)
verbose = util.Get[bool](params, "verbose")
eths = []EthernetInterface{}
)
// make request to SMD endpoint
b, err := client.makeRequest("/Inventory/EthernetInterfaces")
if err != nil {
@ -42,16 +47,14 @@ func (client *SmdClient) FetchEthernetInterfaces(opts ...util.Option) ([]Etherne
}
// unmarshal response body JSON and extract in object
eths := []EthernetInterface{} // []map[string]any{}
err = json.Unmarshal(b, &eths)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
}
// print what we got if verbose is set
params := util.GetParams(opts...)
if verbose, ok := params["verbose"].(bool); ok {
if verbose {
if verbose != nil {
if *verbose {
fmt.Printf("Ethernet Interfaces: %v\n", string(b))
}
}
@ -62,23 +65,26 @@ func (client *SmdClient) FetchEthernetInterfaces(opts ...util.Option) ([]Etherne
// 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(opts ...util.Option) ([]Component, error) {
var (
params = util.GetParams(opts...)
verbose = util.Get[bool](params, "verbose")
comps = []Component{}
)
// make request to SMD endpoint
b, err := client.makeRequest("/State/Components")
if err != nil {
return nil, fmt.Errorf("failed to read HTTP response: %v", err)
return nil, fmt.Errorf("failed to make HTTP request: %v", err)
}
// unmarshal response body JSON and extract in object
comps := []Component{}
err = json.Unmarshal(b, &comps)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
}
// print what we got if verbose is set
params := util.GetParams(opts...)
if verbose, ok := params["verbose"].(bool); ok {
if verbose {
if verbose != nil {
if *verbose {
fmt.Printf("Components: %v\n", string(b))
}
}
@ -86,6 +92,32 @@ func (client *SmdClient) FetchComponents(opts ...util.Option) ([]Component, erro
return comps, nil
}
func (client *SmdClient) FetchRedfishEndpoints(opts ...util.Option) ([]RedfishEndpoint, error) {
var (
params = util.GetParams(opts...)
verbose = util.Get[bool](params, "verbose")
rfs = []RedfishEndpoint{}
)
b, err := client.makeRequest("/Inventory/RedfishEndpoints")
if err != nil {
return nil, fmt.Errorf("failed to make HTTP resquest: %v", err)
}
err = json.Unmarshal(b, &rfs)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
}
if verbose != nil {
if *verbose {
fmt.Printf("Redfish endpoints: %v\n", string(b))
}
}
return rfs, nil
}
func (client *SmdClient) makeRequest(endpoint string) ([]byte, error) {
if client == nil {
return nil, fmt.Errorf("client is nil")

View file

@ -27,7 +27,7 @@ type Config struct {
SmdClient SmdClient `yaml:"smd"`
AccessToken string `yaml:"access-token"`
TemplatePaths map[string]string `yaml:"templates"`
Plugins []string `yaml:"plugins"`
PluginDirs []string `yaml:"plugins"`
Options Options `yaml:"options"`
}
@ -45,7 +45,7 @@ func NewConfig() Config {
"powerman": "templates/powerman.jinja",
"conman": "templates/conman.jinja",
},
Plugins: []string{},
PluginDirs: []string{},
Server: Server{
Host: "127.0.0.1",
Port: 3334,

View file

@ -1,5 +1,7 @@
package configurator
import "encoding/json"
type IPAddr struct {
IpAddress string `json:"IPAddress"`
Network string `json:"Network"`
@ -16,6 +18,38 @@ type EthernetInterface struct {
}
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"`
}
type Node struct {

View file

@ -3,6 +3,7 @@ package generator
import (
"bytes"
"fmt"
"maps"
"os"
"plugin"
@ -10,6 +11,7 @@ import (
"github.com/OpenCHAMI/configurator/internal/util"
"github.com/nikolalohinski/gonja/v2"
"github.com/nikolalohinski/gonja/v2/exec"
"github.com/sirupsen/logrus"
)
type Mappings = map[string]any
@ -19,6 +21,13 @@ type Generator interface {
Generate(config *configurator.Config, opts ...util.Option) ([]byte, error)
}
type Params struct {
Args []string
PluginPaths []string
Target string
Verbose bool
}
func LoadPlugin(path string) (Generator, error) {
p, err := plugin.Open(path)
if err != nil {
@ -45,53 +54,33 @@ func LoadPlugins(dirpath string, opts ...util.Option) (map[string]Generator, err
)
items, _ := os.ReadDir(dirpath)
var LoadGenerator = func(path string) (Generator, error) {
// load each generator plugin
p, err := plugin.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to load plugin: %v", err)
}
// lookup symbol in plugin
symbol, err := p.Lookup("Generator")
if err != nil {
return nil, fmt.Errorf("failed to look up symbol: %v", err)
}
// assert that the loaded symbol is the correct type
gen, ok := symbol.(Generator)
if !ok {
return nil, fmt.Errorf("failed to load the correct symbol type")
}
return gen, nil
}
for _, item := range items {
if item.IsDir() {
subitems, _ := os.ReadDir(item.Name())
for _, subitem := range subitems {
if !subitem.IsDir() {
gen, err := LoadGenerator(subitem.Name())
gen, err := LoadPlugin(subitem.Name())
if err != nil {
fmt.Printf("failed to load generator in directory '%s': %v\n", item.Name(), err)
continue
}
if verbose, ok := params["verbose"].(bool); ok {
if verbose {
fmt.Printf("found plugin '%s'\n", item.Name())
fmt.Printf("-- found plugin '%s'\n", item.Name())
}
}
gens[gen.GetName()] = gen
}
}
} else {
gen, err := LoadGenerator(dirpath + item.Name())
gen, err := LoadPlugin(dirpath + item.Name())
if err != nil {
fmt.Printf("failed to load generator: %v\n", err)
continue
}
if verbose, ok := params["verbose"].(bool); ok {
if verbose {
fmt.Printf("found plugin '%s'\n", dirpath+item.Name())
fmt.Printf("-- found plugin '%s'\n", dirpath+item.Name())
}
}
gens[gen.GetName()] = gen
@ -123,17 +112,15 @@ func WithClient(client configurator.SmdClient) util.Option {
}
}
// Syntactic sugar generic function to get parameter from util.Params.
func Get[T any](params util.Params, key string) *T {
if v, ok := params[key].(T); ok {
return &v
func WithOption(key string, value any) util.Option {
return func(p util.Params) {
p[key] = value
}
return nil
}
// Helper function to get client in generator plugins.
func GetClient(params util.Params) *configurator.SmdClient {
return Get[configurator.SmdClient](params, "client")
return util.Get[configurator.SmdClient](params, "client")
}
func GetParams(opts ...util.Option) util.Params {
@ -144,10 +131,6 @@ func GetParams(opts ...util.Option) util.Params {
return params
}
func Generate(g Generator, config *configurator.Config, opts ...util.Option) {
g.Generate(config, opts...)
}
func ApplyTemplate(path string, mappings map[string]any) ([]byte, error) {
data := exec.NewContext(mappings)
@ -165,3 +148,71 @@ func ApplyTemplate(path string, mappings map[string]any) ([]byte, error) {
return b.Bytes(), nil
}
func Generate(config *configurator.Config, params Params) ([]byte, error) {
// load generator plugins to generate configs or to print
var (
generators = make(map[string]Generator)
client = configurator.SmdClient{
Host: config.SmdClient.Host,
Port: config.SmdClient.Port,
AccessToken: config.AccessToken,
}
)
// make sure that we have a token present before trying to make request
if config.AccessToken == "" {
// TODO: make request to check if request will need token
// check if OCHAMI_ACCESS_TOKEN env var is set if no access token is provided and use that instead
accessToken := os.Getenv("ACCESS_TOKEN")
if accessToken != "" {
config.AccessToken = accessToken
} else {
// TODO: try and fetch token first if it is needed
if params.Verbose {
fmt.Printf("No token found. Attempting to generate config without one...\n")
}
}
}
// load all plugins from params
for _, path := range params.PluginPaths {
if params.Verbose {
fmt.Printf("loading plugins from '%s'\n", path)
}
gens, err := LoadPlugins(path)
if err != nil {
fmt.Printf("failed to load plugins: %v\n", err)
err = nil
continue
}
// add loaded generator plugins to set
maps.Copy(generators, gens)
}
// show available targets then exit
if len(params.Args) == 0 && params.Target == "" {
for g := range generators {
fmt.Printf("-- found generator plugin \"%s\"\n", g)
}
return nil, nil
}
if params.Target == "" {
logrus.Errorf("no target supplied (--target name)")
} else {
// run the generator plugin from target passed
gen := generators[params.Target]
if gen == nil {
return nil, fmt.Errorf("invalid generator target (%s)", params.Target)
}
return gen.Generate(
config,
WithTemplate(gen.GetName()),
WithClient(client),
)
}
return nil, fmt.Errorf("an unknown error has occurred")
}

View file

@ -9,6 +9,7 @@ import (
"time"
configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/generator"
"github.com/OpenCHAMI/jwtauth/v5"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
@ -19,9 +20,15 @@ var (
tokenAuth *jwtauth.JWTAuth = nil
)
type Jwks struct {
Uri string
Retries int
}
type Server struct {
*http.Server
JwksUri string `yaml:"jwks-uri"`
Jwks Jwks `yaml:"jwks"`
GeneratorParams generator.Params
TokenAuth *jwtauth.JWTAuth
}
func New() *Server {
@ -29,11 +36,14 @@ func New() *Server {
Server: &http.Server{
Addr: "localhost:3334",
},
JwksUri: "",
Jwks: Jwks{
Uri: "",
Retries: 5,
},
}
}
func (s *Server) Start(config *configurator.Config) error {
func (s *Server) Serve(config *configurator.Config) error {
// create client just for the server to use to fetch data from SMD
_ = &configurator.SmdClient{
Host: config.SmdClient.Host,
@ -56,6 +66,12 @@ func (s *Server) Start(config *configurator.Config) error {
}
}
var WriteError = func(w http.ResponseWriter, format string, a ...any) {
errmsg := fmt.Sprintf(format, a...)
fmt.Printf(errmsg)
w.Write([]byte(errmsg))
}
// create new go-chi router with its routes
router := chi.NewRouter()
router.Use(middleware.RedirectSlashes)
@ -67,11 +83,19 @@ func (s *Server) Start(config *configurator.Config) error {
jwtauth.Authenticator(tokenAuth),
)
}
r.HandleFunc("/target", func(w http.ResponseWriter, r *http.Request) {
// g := generator.Generator{
// Type: r.URL.Query().Get("type"),
// Template: r.URL.Query().Get("template"),
// }
r.HandleFunc("/generate", func(w http.ResponseWriter, r *http.Request) {
s.GeneratorParams.Target = r.URL.Query().Get("target")
output, err := generator.Generate(config, s.GeneratorParams)
if err != nil {
WriteError(w, "failed to generate config: %v\n", err)
return
}
_, err = w.Write(output)
if err != nil {
WriteError(w, "failed to write response: %v", err)
return
}
// NOTE: we probably don't want to hardcode the types, but should do for now
// if _type == "dhcp" {

View file

@ -35,3 +35,20 @@ func AssertOptionsExist(params Params, opts ...string) []string {
}
return foundKeys
}
func WithDefault[T any](v T) Option {
return func(p Params) {
p["default"] = v
}
}
// Syntactic sugar generic function to get parameter from util.Params.
func Get[T any](params Params, key string, opts ...Option) *T {
if v, ok := params[key].(T); ok {
return &v
}
if defaultValue, ok := params["default"].(T); ok {
return &defaultValue
}
return nil
}