From d77a31c7fed51fc23e0722b120dbcbaf46466cf7 Mon Sep 17 00:00:00 2001 From: David Allen Date: Wed, 19 Jun 2024 14:19:42 -0600 Subject: [PATCH] Rewrote generators to use plugin system with default plugins --- cmd/generate.go | 198 ++++++++++++------ cmd/root.go | 4 +- internal/client.go | 111 +++++++--- internal/config.go | 44 ++-- internal/configurator.go | 11 +- internal/generator/generator.go | 166 ++++++++++++--- internal/generator/plugins/conman/conman.go | 46 ++++ .../generator/plugins/coredhcp/coredhcp.go | 22 ++ internal/generator/plugins/dnsmasq/dnsmasq.go | 89 ++++++++ .../generator/plugins/powerman/powerman.go | 22 ++ internal/generator/plugins/syslog/syslog.go | 22 ++ .../generator/plugins/warewulf/warewulf.go | 44 ++++ internal/schema.go | 2 + internal/server/server.go | 73 +++---- internal/util/params.go | 37 ++++ 15 files changed, 712 insertions(+), 179 deletions(-) create mode 100644 internal/generator/plugins/conman/conman.go create mode 100644 internal/generator/plugins/coredhcp/coredhcp.go create mode 100644 internal/generator/plugins/dnsmasq/dnsmasq.go create mode 100644 internal/generator/plugins/powerman/powerman.go create mode 100644 internal/generator/plugins/syslog/syslog.go create mode 100644 internal/generator/plugins/warewulf/warewulf.go create mode 100644 internal/schema.go create mode 100644 internal/util/params.go diff --git a/cmd/generate.go b/cmd/generate.go index 96553ec..d73d645 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -4,10 +4,11 @@ package cmd import ( + "encoding/json" "fmt" + "maps" "os" "path/filepath" - "strings" configurator "github.com/OpenCHAMI/configurator/internal" "github.com/OpenCHAMI/configurator/internal/generator" @@ -17,16 +18,52 @@ import ( var ( tokenFetchRetries int + pluginPaths []string ) var generateCmd = &cobra.Command{ Use: "generate", - Short: "Generate a config file from system state", + Short: "Generate a config file from state management", Run: func(cmd *cobra.Command, args []string) { - client := configurator.SmdClient{ - Host: config.SmdHost, - Port: config.SmdPort, - AccessToken: config.AccessToken, + // load generator plugins to generate configs or to print + var ( + generators = make(map[string]generator.Generator) + client = configurator.SmdClient{ + Host: config.SmdClient.Host, + Port: config.SmdClient.Port, + AccessToken: config.AccessToken, + } + ) + for _, path := range pluginPaths { + if verbose { + fmt.Printf("loading plugins from '%s'\n", path) + } + gens, err := generator.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 config as JSON and generators if verbose + if verbose { + b, err := json.MarshalIndent(config, "", " ") + if err != nil { + fmt.Printf("failed to marshal config: %v\n", err) + } + fmt.Printf("%v\n", string(b)) + } + + // show available targets then exit + if len(args) == 0 && len(targets) == 0 { + for g := range generators { + fmt.Printf("\tplugin: %s, name:\n", g) + } + os.Exit(0) } // make sure that we have a token present before trying to make request @@ -34,12 +71,14 @@ var generateCmd = &cobra.Command{ // 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("OCHAMI_ACCESS_TOKEN") + accessToken := os.Getenv("ACCESS_TOKEN") if accessToken != "" { config.AccessToken = accessToken } else { // TODO: try and fetch token first if it is needed - fmt.Printf("No token found. Attempting to generate config without one...\n") + if verbose { + fmt.Printf("No token found. Attempting to generate config without one...\n") + } } } @@ -58,82 +97,102 @@ var generateCmd = &cobra.Command{ for _, target := range targets { // split the target and type - tmp := strings.Split(target, ":") + // tmp := strings.Split(target, ":") // make sure each target has at least two args - if len(tmp) < 2 { - message := "target" - if len(tmp) == 1 { - message += fmt.Sprintf(" '%s'", tmp[1]) - } - message += " does not provide enough arguments (args: \"type:template\")" - logrus.Errorf(message) - continue - } - g := generator.Generator{ - Type: tmp[0], - Template: tmp[1], - } + // if len(tmp) < 2 { + // message := "target" + // if len(tmp) == 1 { + // message += fmt.Sprintf(" '%s'", tmp[0]) + // } + // message += " does not provide enough arguments (args: \"type:template\")" + // logrus.Errorf(message) + // continue + // } + // var ( + // _type = tmp[0] + // _template = tmp[1] + // ) + // g := generator.Generator{ + // Type: tmp[0], + // Template: tmp[1], + // } // check if another param is specified - targetPath := "" - if len(tmp) > 2 { - targetPath = tmp[2] + // targetPath := "" + // if len(tmp) > 2 { + // targetPath = tmp[2] + // } + + // run the generator plugin from target passed + gen := generators[target] + if gen == nil { + fmt.Printf("invalid generator target (%s)\n", target) + continue + } + output, err := gen.Generate( + &config, + generator.WithTemplate(gen.GetName()), + generator.WithClient(client), + ) + if err != nil { + fmt.Printf("failed to generate config: %v\n", err) + continue } // NOTE: we probably don't want to hardcode the types, but should do for now - ext := "" - contents := []byte{} - if g.Type == "dhcp" { - // fetch eths from SMD - eths, err := client.FetchEthernetInterfaces() - if err != nil { - logrus.Errorf("failed to fetch DHCP metadata: %v\n", err) - continue - } - if len(eths) <= 0 { - continue - } - // generate a new config from that data - contents, err = g.GenerateDHCP(&config, eths) - if err != nil { - logrus.Errorf("failed to generate DHCP config file: %v\n", err) - continue - } - ext = "conf" - } else if g.Type == "dns" { - // TODO: fetch from SMD - // TODO: generate config from pulled info + // ext := "" + // contents := []byte{} + // if _type == "dhcp" { + // // fetch eths from SMD + // eths, err := client.FetchEthernetInterfaces() + // if err != nil { + // logrus.Errorf("failed to fetch DHCP metadata: %v\n", err) + // continue + // } + // if len(eths) <= 0 { + // continue + // } + // // generate a new config from that data + // contents, err = g.GenerateDHCP(&config, eths) + // if err != nil { + // logrus.Errorf("failed to generate DHCP config file: %v\n", err) + // continue + // } + // ext = "conf" + // } else if g.Type == "dns" { + // // TODO: fetch from SMD + // // TODO: generate config from pulled info - } else if g.Type == "syslog" { + // } else if g.Type == "syslog" { - } else if g.Type == "ansible" { + // } else if g.Type == "ansible" { - } else if g.Type == "warewulf" { + // } else if g.Type == "warewulf" { - } + // } // write config output if no specific targetPath is set - if targetPath == "" { - if outputPath == "" { - // write only to stdout - fmt.Printf("%s\n", "") - } else if outputPath != "" && targetCount == 1 { - // write just a single file using template name - err := os.WriteFile(outputPath, contents, 0o644) - if err != nil { - logrus.Errorf("failed to write config to file: %v", err) - continue - } - } else if outputPath != "" && targetCount > 1 { - // write multiple files in directory using template name - err := os.WriteFile(fmt.Sprintf("%s/%s.%s", filepath.Clean(outputPath), g.Template, ext), contents, 0o644) - if err != nil { - logrus.Errorf("failed to write config to file: %v", err) - continue - } + // if targetPath == "" { + if outputPath == "" { + // write only to stdout + fmt.Printf("%s\n", string(output)) + } else if outputPath != "" && targetCount == 1 { + // write just a single file using template name + err := os.WriteFile(outputPath, output, 0o644) + if err != nil { + logrus.Errorf("failed to write config to file: %v", err) + continue + } + } else if outputPath != "" && targetCount > 1 { + // write multiple files in directory using template name + err := os.WriteFile(fmt.Sprintf("%s/%s.%s", filepath.Clean(outputPath), target, ".conf"), output, 0o644) + if err != nil { + logrus.Errorf("failed to write config to file: %v", err) + continue } } + // } } // for targets } }, @@ -141,6 +200,7 @@ var generateCmd = &cobra.Command{ func init() { generateCmd.Flags().StringSliceVar(&targets, "target", nil, "set the target configs to make") + generateCmd.Flags().StringSliceVar(&pluginPaths, "plugin", nil, "set the generator plugins directory path to shared libraries") generateCmd.Flags().StringVarP(&outputPath, "output", "o", "", "set the output path for config targets") generateCmd.Flags().IntVar(&tokenFetchRetries, "fetch-retries", 5, "set the number of retries to fetch an access token") diff --git a/cmd/root.go b/cmd/root.go index 89f92c8..d376654 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,6 +12,7 @@ import ( var ( configPath string config configurator.Config + verbose bool targets []string outputPath string ) @@ -36,7 +37,8 @@ func Execute() { func init() { cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "set the config path") + rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "./config.yaml", "set the config path") + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "set to enable verbose output") } func initConfig() { diff --git a/internal/client.go b/internal/client.go index 75d1776..68038d9 100644 --- a/internal/client.go +++ b/internal/client.go @@ -6,49 +6,110 @@ import ( "fmt" "io" "net/http" + + "github.com/OpenCHAMI/configurator/internal/util" ) type SmdClient struct { http.Client - Host string - Port int - AccessToken string + Host string `yaml:"host"` + Port int `yaml:"port"` + AccessToken string `yaml:"access-token"` } -func (client *SmdClient) FetchDNS(config *Config) error { - // fetch DNS related information from SMD's endpoint: - return nil +type Params = map[string]any +type Option func(Params) + +func WithVerbose() Option { + return func(p util.Params) { + p["verbose"] = true + } } -func (client *SmdClient) FetchEthernetInterfaces() ([]EthernetInterface, error) { +func NewParams() Params { + return Params{ + "verbose": false, + } +} + +// 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) { + // make request to SMD endpoint + b, err := client.makeRequest("/Inventory/EthernetInterfaces") + if err != nil { + return nil, fmt.Errorf("failed to read HTTP response: %v", err) + } + + // unmarshal response body JSON and extract in object + eths := []EthernetInterface{} // []map[string]any{} + err = json.Unmarshal(b, ðs) + 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 { + fmt.Printf("Ethernet Interfaces: %v\n", string(b)) + } + } + + return eths, 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(opts ...util.Option) ([]Component, error) { + // 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) + } + + // 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 { + fmt.Printf("Components: %v\n", string(b)) + } + } + + return comps, 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:%d/hsm/v2/Inventory/EthernetInterfaces", client.Host, client.Port) - req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer([]byte{})) - // include access token in authorzation header if found - if client.AccessToken != "" { - req.Header.Add("Authorization", "Bearer "+client.AccessToken) - } + // fetch DHCP related information from SMD's endpoint: + url := fmt.Sprintf("%s:%d/hsm/v2%s", client.Host, client.Port, 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) } - b, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read HTTP response: %v", err) - } - - // unmarshal JSON and extract - eths := []EthernetInterface{} // []map[string]any{} - json.Unmarshal(b, ðs) - fmt.Printf("ethernet interfaces: %v\n", string(b)) - - return eths, nil + // read the contents of the response body + return io.ReadAll(res.Body) } diff --git a/internal/config.go b/internal/config.go index c674290..7aabb0a 100644 --- a/internal/config.go +++ b/internal/config.go @@ -8,45 +8,53 @@ import ( "gopkg.in/yaml.v2" ) -type Options struct { - JwksUri string `yaml:"jwks-uri"` - JwksRetries int `yaml:"jwks-retries"` +type Options struct{} + +type Jwks struct { + Uri string `yaml:"uri"` + Retries int `yaml:"retries"` } type Server struct { Host string `yaml:"host"` Port int `yaml:"port"` + Jwks Jwks `yaml:"jwks"` } + type Config struct { Version string `yaml:"version"` - SmdHost string `yaml:"smd-host"` - SmdPort int `yaml:"smd-port"` + Server Server `yaml:"server"` + SmdClient SmdClient `yaml:"smd"` AccessToken string `yaml:"access-token"` TemplatePaths map[string]string `yaml:"templates"` - Server Server `yaml:"server"` + Plugins []string `yaml:"plugins"` Options Options `yaml:"options"` } func NewConfig() Config { return Config{ Version: "", - SmdHost: "http://127.0.0.1", - SmdPort: 27779, - TemplatePaths: map[string]string{ - "dnsmasq": "templates/dhcp/dnsmasq.conf", - "syslog": "templates/syslog/", - "ansible": "templates/ansible", - "powerman": "templates/powerman", - "conman": "templates/conman", + SmdClient: SmdClient{ + Host: "http://127.0.0.1", + Port: 27779, }, + TemplatePaths: map[string]string{ + "dnsmasq": "templates/dnsmasq.jinja", + "syslog": "templates/syslog.jinja", + "ansible": "templates/ansible.jinja", + "powerman": "templates/powerman.jinja", + "conman": "templates/conman.jinja", + }, + Plugins: []string{}, Server: Server{ Host: "127.0.0.1", Port: 3334, + Jwks: Jwks{ + Uri: "", + Retries: 5, + }, }, - Options: Options{ - JwksUri: "", - JwksRetries: 5, - }, + Options: Options{}, } } diff --git a/internal/configurator.go b/internal/configurator.go index d44388a..a20d5e3 100644 --- a/internal/configurator.go +++ b/internal/configurator.go @@ -15,8 +15,11 @@ type EthernetInterface struct { IpAddresses []IPAddr } -type DHCP struct { - Hostname string - MacAddress string - IpAddress []IPAddr +type Component struct { +} + +type Node struct { +} + +type BMC struct { } diff --git a/internal/generator/generator.go b/internal/generator/generator.go index 3d67e26..fabfe0c 100644 --- a/internal/generator/generator.go +++ b/internal/generator/generator.go @@ -2,50 +2,162 @@ package generator import ( "bytes" - "fmt" + "os" + "plugin" configurator "github.com/OpenCHAMI/configurator/internal" + "github.com/OpenCHAMI/configurator/internal/util" "github.com/nikolalohinski/gonja/v2" "github.com/nikolalohinski/gonja/v2/exec" ) -type Generator struct { - Type string - Template string +type Mappings = map[string]any +type Generator interface { + GetName() string + GetGroups() []string + Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) } -func New() *Generator { - return &Generator{} +func LoadPlugin(path string) (Generator, error) { + p, err := plugin.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to load plugin: %v", err) + } + + symbol, err := p.Lookup("Generator") + if err != nil { + return nil, fmt.Errorf("failed to look up symbol: %v", err) + } + + gen, ok := symbol.(Generator) + if !ok { + return nil, fmt.Errorf("failed to load the correct symbol type") + } + return gen, nil } -func (g *Generator) GenerateDNS(config *configurator.Config) { - // generate file using jinja template - // TODO: load template file for DNS - // TODO: substitute DNS data fetched from SMD - // TODO: print generated config file to STDOUT +func LoadPlugins(dirpath string, opts ...util.Option) (map[string]Generator, error) { + // check if verbose option is supplied + var ( + gens = make(map[string]Generator) + params = util.GetParams(opts...) + ) + + 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()) + 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()) + } + } + gens[gen.GetName()] = gen + } + } + } else { + gen, err := LoadGenerator(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()) + } + } + gens[gen.GetName()] = gen + } + } + + return gens, nil } -func (g *Generator) GenerateDHCP(config *configurator.Config, eths []configurator.EthernetInterface) ([]byte, error) { - // generate file using gonja template - path := config.TemplatePaths[g.Template] - fmt.Printf("path: %s\neth count: %v\n", path, len(eths)) +func WithTemplate(_template string) util.Option { + return func(p util.Params) { + if p != nil { + p["template"] = _template + } + } +} + +func WithType(_type string) util.Option { + return func(p util.Params) { + if p != nil { + p["type"] = _type + } + } +} + +func WithClient(client configurator.SmdClient) util.Option { + return func(p util.Params) { + p["client"] = client + } +} + +// 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 + } + return nil +} + +// Helper function to get client in generator plugins. +func GetClient(params util.Params) *configurator.SmdClient { + return Get[configurator.SmdClient](params, "client") +} + +func GetParams(opts ...util.Option) util.Params { + params := util.Params{} + for _, opt := range opts { + opt(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) + + // load jinja template from file t, err := gonja.FromFile(path) if err != nil { return nil, fmt.Errorf("failed to read template from file: %v", err) } - template := "# ========== GENERATED BY OCHAMI CONFIGURATOR ==========\n" - for _, eth := range eths { - if eth.Type == "NodeBMC" { - template += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n" - } else { - template += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n" - } - } - template += "# ======================================================" - data := exec.NewContext(map[string]any{ - "hosts": template, - }) + + // execute/render jinja template b := bytes.Buffer{} if err = t.Execute(&b, data); err != nil { return nil, fmt.Errorf("failed to execute: %v", err) diff --git a/internal/generator/plugins/conman/conman.go b/internal/generator/plugins/conman/conman.go new file mode 100644 index 0000000..1477604 --- /dev/null +++ b/internal/generator/plugins/conman/conman.go @@ -0,0 +1,46 @@ +package main + +import ( + "bytes" + "fmt" + + configurator "github.com/OpenCHAMI/configurator/internal" + "github.com/OpenCHAMI/configurator/internal/generator" + "github.com/OpenCHAMI/configurator/internal/util" + "github.com/nikolalohinski/gonja/v2" + "github.com/nikolalohinski/gonja/v2/exec" +) + +type Conman struct{} + +func (g *Conman) GetName() string { + return "conman" +} + +func (g *Conman) GetGroups() []string { + return []string{"conman"} +} + +func (g *Conman) Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) { + params := generator.GetParams(opts...) + var ( + template = params["template"].(string) + path = config.TemplatePaths[template] + ) + data := exec.NewContext(map[string]any{}) + + t, err := gonja.FromFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read template from file: %v", err) + } + output := "# ========== GENERATED BY OCHAMI CONFIGURATOR ==========\n" + output += "# ======================================================" + b := bytes.Buffer{} + if err = t.Execute(&b, data); err != nil { + return nil, fmt.Errorf("failed to execute: %v", err) + } + + return b.Bytes(), nil +} + +var Generator Conman diff --git a/internal/generator/plugins/coredhcp/coredhcp.go b/internal/generator/plugins/coredhcp/coredhcp.go new file mode 100644 index 0000000..d1d0154 --- /dev/null +++ b/internal/generator/plugins/coredhcp/coredhcp.go @@ -0,0 +1,22 @@ +package main + +import ( + configurator "github.com/OpenCHAMI/configurator/internal" + "github.com/OpenCHAMI/configurator/internal/util" +) + +type CoreDhcp struct{} + +func (g *CoreDhcp) GetName() string { + return "coredhcp" +} + +func (g *CoreDhcp) GetGroups() []string { + return []string{"coredhcp"} +} + +func (g *CoreDhcp) Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) { + return nil, nil +} + +var Generator CoreDhcp diff --git a/internal/generator/plugins/dnsmasq/dnsmasq.go b/internal/generator/plugins/dnsmasq/dnsmasq.go new file mode 100644 index 0000000..c7eb39f --- /dev/null +++ b/internal/generator/plugins/dnsmasq/dnsmasq.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + + configurator "github.com/OpenCHAMI/configurator/internal" + "github.com/OpenCHAMI/configurator/internal/generator" + "github.com/OpenCHAMI/configurator/internal/util" +) + +type DnsMasq struct{} + +func TestGenerateDnsMasq() { + var ( + g = DnsMasq{} + config = &configurator.Config{} + client = configurator.SmdClient{} + ) + g.Generate( + config, + generator.WithTemplate("dnsmasq"), + generator.WithClient(client), + ) +} + +func (g *DnsMasq) GetName() string { + return "dnsmasq" +} + +func (g *DnsMasq) GetGroups() []string { + return []string{"dnsmasq"} +} + +func (g *DnsMasq) Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) { + // make sure we have a valid config first + if config == nil { + return nil, fmt.Errorf("invalid config (config is nil)") + } + + // set all the defaults for variables + var ( + params = generator.GetParams(opts...) + template = params["template"].(string) // required param + path = config.TemplatePaths[template] + eths []configurator.EthernetInterface = nil + err error = nil + ) + + // if we have a client, try making the request for the ethernet interfaces + if client, ok := params["client"].(configurator.SmdClient); ok { + eths, err = client.FetchEthernetInterfaces(opts...) + if err != nil { + return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err) + } + } + + // check if we have the required params first + if eths == nil { + return nil, fmt.Errorf("invalid ethernet interfaces (variable is nil)") + } + if len(eths) <= 0 { + return nil, fmt.Errorf("no ethernet interfaces found") + } + + // print message if verbose param found + if verbose, ok := params["verbose"].(bool); ok { + if verbose { + fmt.Printf("path: %s\neth count: %v\n", path, len(eths)) + } + } + + // format output to write to config file + output := "# ========== GENERATED BY OCHAMI CONFIGURATOR ==========\n" + for _, eth := range eths { + if eth.Type == "NodeBMC" { + output += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n" + } else { + output += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n" + } + } + output += "# ======================================================" + + // apply template substitutions and return output as byte array + return generator.ApplyTemplate(path, generator.Mappings{ + "hosts": output, + }) +} + +var Generator DnsMasq diff --git a/internal/generator/plugins/powerman/powerman.go b/internal/generator/plugins/powerman/powerman.go new file mode 100644 index 0000000..6c2b5eb --- /dev/null +++ b/internal/generator/plugins/powerman/powerman.go @@ -0,0 +1,22 @@ +package main + +import ( + configurator "github.com/OpenCHAMI/configurator/internal" + "github.com/OpenCHAMI/configurator/internal/util" +) + +type Powerman struct{} + +func (g *Powerman) GetName() string { + return "powerman" +} + +func (g *Powerman) GetGroups() []string { + return []string{"powerman"} +} + +func (g *Powerman) Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) { + return nil, nil +} + +var Generator Powerman diff --git a/internal/generator/plugins/syslog/syslog.go b/internal/generator/plugins/syslog/syslog.go new file mode 100644 index 0000000..db59ea6 --- /dev/null +++ b/internal/generator/plugins/syslog/syslog.go @@ -0,0 +1,22 @@ +package main + +import ( + configurator "github.com/OpenCHAMI/configurator/internal" + "github.com/OpenCHAMI/configurator/internal/util" +) + +type Syslog struct{} + +func (g *Syslog) GetName() string { + return "syslog" +} + +func (g *Syslog) GetGroups() []string { + return []string{"log"} +} + +func (g *Syslog) Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) { + return nil, nil +} + +var Generator Syslog diff --git a/internal/generator/plugins/warewulf/warewulf.go b/internal/generator/plugins/warewulf/warewulf.go new file mode 100644 index 0000000..9de624b --- /dev/null +++ b/internal/generator/plugins/warewulf/warewulf.go @@ -0,0 +1,44 @@ +package main + +import ( + "bytes" + "fmt" + + configurator "github.com/OpenCHAMI/configurator/internal" + "github.com/nikolalohinski/gonja/v2" + "github.com/nikolalohinski/gonja/v2/exec" +) + +type Warewulf struct{} + +func (g *Warewulf) GetName() string { + return "warewulf" +} + +func (g *Warewulf) GetGroups() []string { + return []string{"warewulf"} +} + +func (g *Warewulf) Generate(config *configurator.Config, template string) ([]byte, error) { + var ( + path = config.TemplatePaths[template] + ) + + t, err := gonja.FromFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read template from file: %v", err) + } + output := "# ========== GENERATED BY OCHAMI CONFIGURATOR ==========\n" + + output += "# ======================================================" + data := exec.NewContext(map[string]any{ + "hosts": output, + }) + b := bytes.Buffer{} + if err = t.Execute(&b, data); err != nil { + return nil, fmt.Errorf("failed to execute: %v", err) + } + return nil, nil +} + +var Generator Warewulf diff --git a/internal/schema.go b/internal/schema.go new file mode 100644 index 0000000..dc794a1 --- /dev/null +++ b/internal/schema.go @@ -0,0 +1,2 @@ +// TODO: implement a way to fetch schemas from node orchestrator +package configurator diff --git a/internal/server/server.go b/internal/server/server.go index 6d3c324..5f57073 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -9,7 +9,6 @@ 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" @@ -22,29 +21,33 @@ var ( type Server struct { *http.Server + JwksUri string `yaml:"jwks-uri"` } func New() *Server { return &Server{ - Server: &http.Server{}, + Server: &http.Server{ + Addr: "localhost:3334", + }, + JwksUri: "", } } func (s *Server) Start(config *configurator.Config) error { // create client just for the server to use to fetch data from SMD - client := &configurator.SmdClient{ - Host: config.SmdHost, - Port: config.SmdPort, + _ = &configurator.SmdClient{ + Host: config.SmdClient.Host, + Port: config.SmdClient.Port, } // set the server address with config values s.Server.Addr = fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port) // fetch JWKS public key from authorization server - if config.Options.JwksUri != "" && tokenAuth == nil { - for i := 0; i < config.Options.JwksRetries; i++ { + if config.Server.Jwks.Uri != "" && tokenAuth == nil { + for i := 0; i < config.Server.Jwks.Retries; i++ { var err error - tokenAuth, err = configurator.FetchPublicKeyFromURL(config.Options.JwksUri) + tokenAuth, err = configurator.FetchPublicKeyFromURL(config.Server.Jwks.Uri) if err != nil { logrus.Errorf("failed to fetch JWKS: %w", err) continue @@ -58,42 +61,42 @@ func (s *Server) Start(config *configurator.Config) error { router.Use(middleware.RedirectSlashes) router.Use(middleware.Timeout(60 * time.Second)) router.Group(func(r chi.Router) { - if config.Options.JwksUri != "" { + if config.Server.Jwks.Uri != "" { r.Use( jwtauth.Verifier(tokenAuth), 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"), - } + // g := generator.Generator{ + // Type: r.URL.Query().Get("type"), + // Template: r.URL.Query().Get("template"), + // } // NOTE: we probably don't want to hardcode the types, but should do for now - if g.Type == "dhcp" { - // fetch eths from SMD - eths, err := client.FetchEthernetInterfaces() - if err != nil { - logrus.Errorf("failed to fetch DHCP metadata: %v\n", err) - w.Write([]byte("An error has occurred")) - return - } - if len(eths) <= 0 { - logrus.Warnf("no ethernet interfaces found") - w.Write([]byte("no ethernet interfaces found")) - return - } - // generate a new config from that data + // if _type == "dhcp" { + // // fetch eths from SMD + // eths, err := client.FetchEthernetInterfaces() + // if err != nil { + // logrus.Errorf("failed to fetch DHCP metadata: %v\n", err) + // w.Write([]byte("An error has occurred")) + // return + // } + // if len(eths) <= 0 { + // logrus.Warnf("no ethernet interfaces found") + // w.Write([]byte("no ethernet interfaces found")) + // return + // } + // // generate a new config from that data - b, err := g.GenerateDHCP(config, eths) - if err != nil { - logrus.Errorf("failed to generate DHCP: %v", err) - w.Write([]byte("An error has occurred.")) - return - } - w.Write(b) - } + // // b, err := g.GenerateDHCP(config, eths) + // if err != nil { + // logrus.Errorf("failed to generate DHCP: %v", err) + // w.Write([]byte("An error has occurred.")) + // return + // } + // w.Write(b) + // } }) r.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) { // TODO: handle GET request diff --git a/internal/util/params.go b/internal/util/params.go new file mode 100644 index 0000000..cc006fc --- /dev/null +++ b/internal/util/params.go @@ -0,0 +1,37 @@ +package util + +import ( + "slices" + + "golang.org/x/exp/maps" +) + +type Params = map[string]any +type Option func(Params) + +func GetParams(opts ...Option) Params { + params := Params{} + for _, opt := range opts { + opt(params) + } + return params +} + +func OptionExists(params Params, opt string) bool { + var k []string = maps.Keys(params) + return slices.Contains(k, opt) +} + +// Assert that the options exists within the params map +func AssertOptionsExist(params Params, opts ...string) []string { + foundKeys := []string{} + for k := range params { + index := slices.IndexFunc(opts, func(s string) bool { + return s == k + }) + if index >= 0 { + foundKeys = append(foundKeys, k) + } + } + return foundKeys +}