diff --git a/pkg/generator/conman.go b/pkg/generator/conman.go index 53358b5..ccadaa6 100644 --- a/pkg/generator/conman.go +++ b/pkg/generator/conman.go @@ -4,6 +4,8 @@ import ( "fmt" configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/client" + "github.com/OpenCHAMI/configurator/pkg/config" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -21,32 +23,20 @@ func (g *Conman) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *Conman) Generate(config *configurator.Config, opts ...util.Option) (FileMap, error) { +func (g *Conman) Generate(config *config.Config, params Params) (FileMap, error) { var ( - params = GetParams(opts...) - client = GetClient(params) - targetKey = params["target"].(string) // required param - target = config.Targets[targetKey] - eps []configurator.RedfishEndpoint = nil - err error = nil - // serverOpts = "" - // globalOpts = "" - consoles = "" + smdClient = client.NewSmdClient(params.ClientOpts...) + eps = []configurator.RedfishEndpoint{} + err error = nil + consoles = "" ) // fetch required data from SMD to create config - if client != nil { - eps, err = client.FetchRedfishEndpoints(opts...) - if err != nil { - return nil, fmt.Errorf("failed to fetch redfish endpoints with client: %v", err) - } + eps, err = smdClient.FetchRedfishEndpoints(params.Verbose) + if err != nil { + return nil, fmt.Errorf("failed to fetch redfish endpoints with client: %v", err) } - // add any additional conman or server opts - // if extraOpts, ok := params["opts"].(map[string]any); ok { - - // } - // format output to write to config file consoles = "# ========== DYNAMICALLY GENERATED BY OPENCHAMI CONFIGURATOR ==========\n" for _, ep := range eps { @@ -55,12 +45,12 @@ func (g *Conman) Generate(config *configurator.Config, opts ...util.Option) (Fil consoles += "# =====================================================================" // apply template substitutions and return output as byte array - return ApplyTemplateFromFiles(Mappings{ + return ApplyTemplates(Mappings{ "plugin_name": g.GetName(), "plugin_version": g.GetVersion(), "plugin_description": g.GetDescription(), "server_opts": "", "global_opts": "", "consoles": consoles, - }, target.TemplatePaths...) + }, params.Templates) } diff --git a/pkg/generator/dhcpd.go b/pkg/generator/dhcpd.go index f2ce520..7425a57 100644 --- a/pkg/generator/dhcpd.go +++ b/pkg/generator/dhcpd.go @@ -4,6 +4,8 @@ import ( "fmt" configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/client" + "github.com/OpenCHAMI/configurator/pkg/config" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -21,23 +23,18 @@ func (g *DHCPd) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *DHCPd) Generate(config *configurator.Config, opts ...util.Option) (FileMap, error) { +func (g *DHCPd) Generate(config *config.Config, params Params) (FileMap, error) { var ( - params = GetParams(opts...) - client = GetClient(params) - targetKey = params["target"].(string) - target = config.Targets[targetKey] - compute_nodes = "" - eths []configurator.EthernetInterface = nil - err error = nil + smdClient = client.NewSmdClient(params.ClientOpts...) + eths = []configurator.EthernetInterface{} + computeNodes = "" + err error = nil ) // - if client != nil { - eths, err = client.FetchEthernetInterfaces(opts...) - if err != nil { - return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %w", err) - } + eths, err = smdClient.FetchEthernetInterfaces(params.Verbose) + if err != nil { + return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %w", err) } // check if we have the required params first @@ -49,25 +46,23 @@ func (g *DHCPd) Generate(config *configurator.Config, opts ...util.Option) (File } // format output to write to config file - compute_nodes = "# ========== DYNAMICALLY GENERATED BY OPENCHAMI CONFIGURATOR ==========\n" + computeNodes = "# ========== DYNAMICALLY GENERATED BY OPENCHAMI CONFIGURATOR ==========\n" for _, eth := range eths { if len(eth.IpAddresses) == 0 { continue } - compute_nodes += fmt.Sprintf("host %s { hardware ethernet %s; fixed-address %s} ", eth.ComponentId, eth.MacAddress, eth.IpAddresses[0]) + computeNodes += fmt.Sprintf("host %s { hardware ethernet %s; fixed-address %s} ", eth.ComponentId, eth.MacAddress, eth.IpAddresses[0]) } - compute_nodes += "# =====================================================================" + computeNodes += "# =====================================================================" - if verbose, ok := params["verbose"].(bool); ok { - if verbose { - fmt.Printf("") - } + if params.Verbose { + fmt.Printf("") } - return ApplyTemplateFromFiles(Mappings{ + return ApplyTemplates(Mappings{ "plugin_name": g.GetName(), "plugin_version": g.GetVersion(), "plugin_description": g.GetDescription(), - "compute_nodes": compute_nodes, + "compute_nodes": computeNodes, "node_entries": "", - }, target.TemplatePaths...) + }, params.Templates) } diff --git a/pkg/generator/dnsmasq.go b/pkg/generator/dnsmasq.go index ab5e648..83bf1d6 100644 --- a/pkg/generator/dnsmasq.go +++ b/pkg/generator/dnsmasq.go @@ -2,9 +2,10 @@ package generator import ( "fmt" - "strings" configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/client" + "github.com/OpenCHAMI/configurator/pkg/config" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -22,7 +23,7 @@ func (g *DNSMasq) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *DNSMasq) Generate(config *configurator.Config, opts ...util.Option) (FileMap, error) { +func (g *DNSMasq) Generate(config *config.Config, params Params) (FileMap, error) { // make sure we have a valid config first if config == nil { return nil, fmt.Errorf("invalid config (config is nil)") @@ -30,20 +31,15 @@ func (g *DNSMasq) Generate(config *configurator.Config, opts ...util.Option) (Fi // set all the defaults for variables var ( - params = GetParams(opts...) - client = GetClient(params) - targetKey = params["target"].(string) // required param - target = config.Targets[targetKey] - eths []configurator.EthernetInterface = nil - err error = nil + smdClient = client.NewSmdClient(params.ClientOpts...) + eths = []configurator.EthernetInterface{} + err error = nil ) // if we have a client, try making the request for the ethernet interfaces - if client != nil { - eths, err = client.FetchEthernetInterfaces(opts...) - if err != nil { - return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err) - } + eths, err = smdClient.FetchEthernetInterfaces(params.Verbose) + if err != nil { + return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err) } // check if we have the required params first @@ -54,13 +50,6 @@ func (g *DNSMasq) Generate(config *configurator.Config, opts ...util.Option) (Fi 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("template: \n%s\nethernet interfaces found: %v\n", strings.Join(target.TemplatePaths, "\n\t"), len(eths)) - } - } - // format output to write to config file output := "# ========== DYNAMICALLY GENERATED BY OPENCHAMI CONFIGURATOR ==========\n" for _, eth := range eths { @@ -73,10 +62,10 @@ func (g *DNSMasq) Generate(config *configurator.Config, opts ...util.Option) (Fi output += "# =====================================================================" // apply template substitutions and return output as byte array - return ApplyTemplateFromFiles(Mappings{ + return ApplyTemplates(Mappings{ "plugin_name": g.GetName(), "plugin_version": g.GetVersion(), "plugin_description": g.GetDescription(), "dhcp-hosts": output, - }, target.TemplatePaths...) + }, params.Templates) } diff --git a/pkg/generator/example.go b/pkg/generator/example.go index b8b5c1d..f18abe4 100644 --- a/pkg/generator/example.go +++ b/pkg/generator/example.go @@ -1,13 +1,9 @@ -//go:build example || plugins -// +build example plugins - package generator import ( "fmt" - configurator "github.com/OpenCHAMI/configurator/pkg" - "github.com/OpenCHAMI/configurator/pkg/generator" + "github.com/OpenCHAMI/configurator/pkg/config" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -27,11 +23,9 @@ func (g *Example) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *Example) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) { +func (g *Example) Generate(config *config.Config, params Params) (FileMap, error) { g.Message = ` This is an example generator plugin. See the file in 'internal/generator/plugins/example/example.go' on information about constructing plugins and plugin requirements.` - return generator.FileMap{"example": []byte(g.Message)}, nil + return FileMap{"example": []byte(g.Message)}, nil } - -var Generator Example diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index e705e16..a56c026 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -1,7 +1,6 @@ package generator import ( - "bytes" "fmt" "io/fs" "os" @@ -9,9 +8,9 @@ import ( "plugin" configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/client" + "github.com/OpenCHAMI/configurator/pkg/config" "github.com/OpenCHAMI/configurator/pkg/util" - "github.com/nikolalohinski/gonja/v2" - "github.com/nikolalohinski/gonja/v2/exec" "github.com/rs/zerolog/log" ) @@ -19,7 +18,6 @@ type ( Mappings map[string]any FileMap map[string][]byte FileList [][]byte - Template []byte // Generator interface used to define how files are created. Plugins can // be created entirely independent of the main driver program. @@ -27,17 +25,7 @@ type ( GetName() string GetVersion() string GetDescription() string - Generate(config *configurator.Config, opts ...util.Option) (FileMap, error) - } - - // Params defined and used by the "generate" subcommand. - Params struct { - Args []string - TemplatePaths []string - PluginPath string - Target string - Client *configurator.SmdClient - Verbose bool + Generate(config *config.Config, params Params) (FileMap, error) } ) @@ -47,8 +35,7 @@ func createDefaultGenerators() map[string]Generator { var ( generatorMap = map[string]Generator{} generators = []Generator{ - &Conman{}, &DHCPd{}, &DNSMasq{}, &Hostfile{}, - &Powerman{}, &Syslog{}, &Warewulf{}, + &Conman{}, &DHCPd{}, &DNSMasq{}, &Warewulf{}, &Example{}, } ) for _, g := range generators { @@ -129,11 +116,11 @@ func LoadPlugin(path string) (Generator, error) { // // Returns a map of generators. Each generator can be accessed by the name // returned by the generator.GetName() implemented. -func LoadPlugins(dirpath string, opts ...util.Option) (map[string]Generator, error) { +func LoadPlugins(dirpath string, opts ...Option) (map[string]Generator, error) { // check if verbose option is supplied var ( generators = make(map[string]Generator) - params = util.ToDict(opts...) + params = ToParams(opts...) ) // @@ -150,7 +137,7 @@ func LoadPlugins(dirpath string, opts ...util.Option) (map[string]Generator, err } // show the plugins found if verbose flag is set - if params.GetVerbose() { + if params.Verbose { fmt.Printf("-- found plugin '%s'\n", gen.GetName()) } @@ -163,239 +150,33 @@ func LoadPlugins(dirpath string, opts ...util.Option) (map[string]Generator, err return nil, fmt.Errorf("failed to walk directory: %w", err) } - // items, _ := os.ReadDir(dirpath) - // for _, item := range items { - // if item.IsDir() { - // subitems, _ := os.ReadDir(item.Name()) - // for _, subitem := range subitems { - // if !subitem.IsDir() { - // 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()) - // } - // } - // gens[gen.GetName()] = gen - // } - // } - // } else { - // gen, err := LoadPlugin(dirpath + item.Name()) - // if err != nil { - // fmt.Printf("failed to load plugin: %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 generators, nil } -func LoadTemplate(path string) (Template, error) { - // skip loading template if path is a directory with no error - if isDir, err := util.IsDirectory(path); err == nil && isDir { - return nil, nil - } else if err != nil { - return nil, fmt.Errorf("failed to test if template path is directory: %w", err) - } - - // try and read the contents of the file - // NOTE: we don't care if this is actually a Jinja template - // or not...at least for now. - return os.ReadFile(path) -} - -func LoadTemplates(paths []string, opts ...util.Option) (map[string]Template, error) { - var ( - templates = make(map[string]Template) - params = util.ToDict(opts...) - ) - - for _, path := range paths { - err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error { - // skip trying to load generator plugin if directory or error - if info.IsDir() || err != nil { - return nil - } - - // load the contents of the template - template, err := LoadTemplate(path) - if err != nil { - return fmt.Errorf("failed to load generator in directory '%s': %w", path, err) - } - - // show the templates loaded if verbose flag is set - if params.GetVerbose() { - fmt.Printf("-- loaded tempalte '%s'\n", path) - } - - // map each template by the path it was loaded from - templates[path] = template - return nil - }) - - if err != nil { - return nil, fmt.Errorf("failed to walk directory: %w", err) - } - } - - return templates, nil -} - -// Option to specify "target" in parameter map. This is used to set which generator -// to use to generate a config file. -func WithTarget(target string) util.Option { - return func(p util.Params) { - if p != nil { - p["target"] = target - } - } -} - -// Option to specify "type" in parameter map. This is not currently used. -func WithType(_type string) util.Option { - return func(p util.Params) { - if p != nil { - p["type"] = _type - } - } -} - -// Option to the plugin to load -func WithPlugin(path string) util.Option { - return func(p util.Params) { - if p != nil { - plugin, err := LoadPlugin(path) - if err != nil { - return - } - p["plugin"] = plugin - } - } -} - -func WithTemplates(paths []string) util.Option { - return func(p util.Params) { - if p != nil { - templates, err := LoadTemplates(paths) - if err != nil { - - } - p["templates"] = templates - } - } -} - -// Option to a specific client to include in implementing plugin generator.Generate(). -// -// NOTE: This may be changed to pass some kind of client interface as an argument in -// the future instead. -func WithClient(client configurator.SmdClient) util.Option { - return func(p util.Params) { - p["client"] = client - } -} - -// Helper function to get client in generator.Generate() plugin implementations. -func GetClient(params util.Params) *configurator.SmdClient { - return util.Get[configurator.SmdClient](params, "client") -} - -// Helper function to get the target in generator.Generate() plugin implementations. -func GetTarget(config *configurator.Config, key string) configurator.Target { - return config.Targets[key] -} - -// Helper function to load all options set with With*() into parameter map. -func GetParams(opts ...util.Option) util.Params { - params := util.Params{} - for _, opt := range opts { - opt(params) - } - return params -} - -// Wrapper function to slightly abstract away some of the nuances with using gonja -// into a single function call. This function is *mostly* for convenience and -// simplication. If no paths are supplied, then no templates will be applied and -// there will be no output. -// -// The "FileList" returns a slice of byte arrays in the same order as the argument -// list supplied, but with the Jinja templating applied. -func ApplyTemplates(mappings Mappings, contents ...[]byte) (FileList, error) { - var ( - data = exec.NewContext(mappings) - outputs = FileList{} - ) - - for _, b := range contents { - // load jinja template from file - t, err := gonja.FromBytes(b) - if err != nil { - return nil, fmt.Errorf("failed to read template from file: %w", err) - } - - // execute/render jinja template - b := bytes.Buffer{} - if err = t.Execute(&b, data); err != nil { - return nil, fmt.Errorf("failed to execute: %w", err) - } - outputs = append(outputs, b.Bytes()) - } - - return outputs, nil -} - -// Wrapper function similiar to "ApplyTemplates" but takes file paths as arguments. -// This function will load templates from a file instead of using file contents. -func ApplyTemplateFromFiles(mappings Mappings, paths ...string) (FileMap, error) { - var ( - data = exec.NewContext(mappings) - outputs = FileMap{} - ) - - for _, path := range paths { - // load jinja template from file - t, err := gonja.FromFile(path) - if err != nil { - return nil, fmt.Errorf("failed to read template from file: %w", err) - } - - // execute/render jinja template - b := bytes.Buffer{} - if err = t.Execute(&b, data); err != nil { - return nil, fmt.Errorf("failed to execute: %w", err) - } - outputs[path] = b.Bytes() - } - - return outputs, nil -} - // Generate() is the main function to generate a collection of files and returns them as a map. // This function only expects a path to a plugin and paths to a collection of templates to // be used. This function will only load the plugin on-demand and fetch resources as needed. -func Generate(config *configurator.Config, params Params) (FileMap, error) { +// +// This function requires that a target and plugin path be set at minimum. +func Generate(plugin string, params Params) (FileMap, error) { var ( - gen Generator - client = configurator.NewSmdClient() + generator Generator + ok bool + err error ) - return gen.Generate( - config, - WithPlugin(params.PluginPath), - WithTemplates(params.TemplatePaths), - WithClient(client), - ) + // check if generator is built-in first before loading external plugin + generator, ok = DefaultGenerators[plugin] + if !ok { + // only load the plugin needed for this target if we don't find default + log.Error().Msg("could not find target in default generators") + generator, err = LoadPlugin(plugin) + if err != nil { + return nil, fmt.Errorf("failed to load plugin from file: %v", err) + } + } + + return generator.Generate(nil, params) } // Main function to generate a collection of files as a map with the path as the key and @@ -407,59 +188,66 @@ func Generate(config *configurator.Config, params Params) (FileMap, error) { // It is also call when running the configurator as a service with the "/generate" route. // // TODO: Separate loading plugins so we can load them once when running as a service. -func GenerateWithTarget(config *configurator.Config, params Params) (FileMap, error) { +func GenerateWithTarget(config *config.Config, target string) (FileMap, error) { // load generator plugins to generate configs or to print var ( - client configurator.SmdClient - target configurator.Target - generator Generator - err error - ok bool + opts []client.Option + targetInfo configurator.Target + generator Generator + params Params + err error + ok bool ) - // check if we have a client from params first and create new one if not - if params.Client == nil { - client = configurator.NewSmdClient( - configurator.WithHost(config.SmdClient.Host), - configurator.WithPort(config.SmdClient.Port), - configurator.WithAccessToken(config.AccessToken), - configurator.WithCertPoolFile(config.CertPath), - ) - } else { - client = *params.Client - } - // check if a target is supplied - if len(params.Args) == 0 && params.Target == "" { + if target == "" { return nil, fmt.Errorf("must specify a target") } // load target information from config - target, ok = config.Targets[params.Target] + targetInfo, ok = config.Targets[target] if !ok { - return nil, fmt.Errorf("target not found in config") + log.Warn().Msg("target not found in config") } - // if plugin path specified from CLI, use that instead - if params.PluginPath != "" { - target.PluginPath = params.PluginPath + // if no plugin supplied in config target, then using the target supplied + if targetInfo.Plugin == "" { + targetInfo.Plugin = target } // check if generator is built-in first before loading - generator, ok = DefaultGenerators[params.Target] + generator, ok = DefaultGenerators[target] if !ok { // only load the plugin needed for this target if we don't find default - log.Error().Msg("did not find target in default generators") - generator, err = LoadPlugin(target.PluginPath) + log.Error().Msg("could not find target in default generators") + generator, err = LoadPlugin(targetInfo.Plugin) if err != nil { - return nil, fmt.Errorf("failed to load plugin: %w", err) + return nil, fmt.Errorf("failed to load plugin: %v", err) } } + // prepare params to pass into generator + params.Templates = map[string]Template{} + for _, templatePath := range targetInfo.TemplatePaths { + template := Template{} + template.LoadFromFile(templatePath) + params.Templates[templatePath] = template + } + + // set the client options + if config.AccessToken != "" { + params.ClientOpts = append(opts, client.WithAccessToken(config.AccessToken)) + } + if config.CertPath != "" { + params.ClientOpts = append(opts, client.WithCertPoolFile(config.CertPath)) + } + + // load files that are not to be copied + params.Files, err = LoadFiles(targetInfo.FilePaths...) + if err != nil { + return nil, fmt.Errorf("failed to load files to copy: %v", err) + } + // run the generator plugin from target passed - return generator.Generate( - config, - WithTarget(generator.GetName()), - WithClient(client), - ) + return generator.Generate(config, params) } diff --git a/pkg/generator/hostfile.go b/pkg/generator/hostfile.go index e998714..7ce26c8 100644 --- a/pkg/generator/hostfile.go +++ b/pkg/generator/hostfile.go @@ -3,7 +3,7 @@ package generator import ( "fmt" - configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/config" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -21,6 +21,6 @@ func (g *Hostfile) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *Hostfile) Generate(config *configurator.Config, opts ...util.Option) (FileMap, error) { +func (g *Hostfile) Generate(config *config.Config, opts ...Option) (FileMap, error) { return nil, fmt.Errorf("plugin does not implement generation function") } diff --git a/pkg/generator/params.go b/pkg/generator/params.go new file mode 100644 index 0000000..e54420a --- /dev/null +++ b/pkg/generator/params.go @@ -0,0 +1,43 @@ +package generator + +import ( + configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/client" + "github.com/OpenCHAMI/configurator/pkg/config" +) + +type ( + // Params used by the generator + Params struct { + Templates map[string]Template + Files map[string][]byte + ClientOpts []client.Option + Verbose bool + } + Option func(Params) +) + +func ToParams(opts ...Option) Params { + params := Params{} + for _, opt := range opts { + opt(params) + } + return params +} + +func WithClientOpts(opts ...client.Option) Option { + return func(p Params) { + p.ClientOpts = opts + } +} + +func WithTemplates(templates map[string]Template) Option { + return func(p Params) { + p.Templates = templates + } +} + +// Helper function to get the target in generator.Generate() plugin implementations. +func GetTarget(config *config.Config, key string) configurator.Target { + return config.Targets[key] +} diff --git a/pkg/generator/powerman.go b/pkg/generator/powerman.go index 36be6fc..08745e5 100644 --- a/pkg/generator/powerman.go +++ b/pkg/generator/powerman.go @@ -3,7 +3,7 @@ package generator import ( "fmt" - configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/config" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -21,6 +21,6 @@ func (g *Powerman) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *Powerman) Generate(config *configurator.Config, opts ...util.Option) (FileMap, error) { +func (g *Powerman) Generate(config *config.Config, opts ...Option) (FileMap, error) { return nil, fmt.Errorf("plugin does not implement generation function") } diff --git a/pkg/generator/syslog.go b/pkg/generator/syslog.go index 463f727..67b28cf 100644 --- a/pkg/generator/syslog.go +++ b/pkg/generator/syslog.go @@ -3,7 +3,7 @@ package generator import ( "fmt" - configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/config" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -21,6 +21,6 @@ func (g *Syslog) GetDescription() string { return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName()) } -func (g *Syslog) Generate(config *configurator.Config, opts ...util.Option) (FileMap, error) { +func (g *Syslog) Generate(config *config.Config, opts ...Option) (FileMap, error) { return nil, fmt.Errorf("plugin does not implement generation function") } diff --git a/pkg/generator/templates.go b/pkg/generator/templates.go new file mode 100644 index 0000000..6d4ae5d --- /dev/null +++ b/pkg/generator/templates.go @@ -0,0 +1,95 @@ +package generator + +import ( + "bytes" + "fmt" + "os" + + "github.com/OpenCHAMI/configurator/pkg/util" + "github.com/nikolalohinski/gonja/v2" + "github.com/nikolalohinski/gonja/v2/exec" +) + +type Template struct { + Contents []byte `json:"contents"` +} + +func (t *Template) LoadFromFile(path string) error { + // skip loading template if path is a directory with no error + if isDir, err := util.IsDirectory(path); err == nil && isDir { + return nil + } else if err != nil { + return fmt.Errorf("failed to test if template path is directory: %w", err) + } + + // try and read the contents of the file + // NOTE: we don't care if this is actually a Jinja template + // or not...at least for now. + contents, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read file: %v", err) + } + t.Contents = contents + return nil +} + +func (t *Template) IsEmpty() bool { + return len(t.Contents) <= 0 +} + +// Wrapper function to slightly abstract away some of the nuances with using gonja +// into a single function call. This function is *mostly* for convenience and +// simplication. If no paths are supplied, then no templates will be applied and +// there will be no output. +// +// The "FileList" returns a slice of byte arrays in the same order as the argument +// list supplied, but with the Jinja templating applied. +func ApplyTemplates(mappings Mappings, templates map[string]Template) (FileMap, error) { + var ( + data = exec.NewContext(mappings) + outputs = FileMap{} + ) + + for path, template := range templates { + // load jinja template from file + t, err := gonja.FromBytes(template.Contents) + if err != nil { + return nil, fmt.Errorf("failed to read template from file: %w", err) + } + + // execute/render jinja template + b := bytes.Buffer{} + if err = t.Execute(&b, data); err != nil { + return nil, fmt.Errorf("failed to execute: %w", err) + } + outputs[path] = b.Bytes() + } + + return outputs, nil +} + +// Wrapper function similiar to "ApplyTemplates" but takes file paths as arguments. +// This function will load templates from a file instead of using file contents. +func ApplyTemplateFromFiles(mappings Mappings, paths ...string) (FileMap, error) { + var ( + data = exec.NewContext(mappings) + outputs = FileMap{} + ) + + for _, path := range paths { + // load jinja template from file + t, err := gonja.FromFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read template from file: %w", err) + } + + // execute/render jinja template + b := bytes.Buffer{} + if err = t.Execute(&b, data); err != nil { + return nil, fmt.Errorf("failed to execute: %w", err) + } + outputs[path] = b.Bytes() + } + + return outputs, nil +} diff --git a/pkg/generator/warewulf.go b/pkg/generator/warewulf.go index 49b0c2c..cbc4129 100644 --- a/pkg/generator/warewulf.go +++ b/pkg/generator/warewulf.go @@ -3,9 +3,9 @@ package generator import ( "fmt" "maps" - "strings" - configurator "github.com/OpenCHAMI/configurator/pkg" + "github.com/OpenCHAMI/configurator/pkg/client" + "github.com/OpenCHAMI/configurator/pkg/config" "github.com/OpenCHAMI/configurator/pkg/util" ) @@ -23,22 +23,15 @@ func (g *Warewulf) GetDescription() string { return "Configurator generator plugin for 'warewulf' config files." } -func (g *Warewulf) Generate(config *configurator.Config, opts ...util.Option) (FileMap, error) { +func (g *Warewulf) Generate(config *config.Config, params Params) (FileMap, error) { var ( - params = GetParams(opts...) - client = GetClient(params) - targetKey = params["target"].(string) - target = config.Targets[targetKey] - outputs = make(FileMap, len(target.FilePaths)+len(target.TemplatePaths)) + smdClient = client.NewSmdClient(params.ClientOpts...) + outputs = make(FileMap, len(params.Templates)) + nodeEntries = "" ) - // check if our client is included and is valid - if client == nil { - return nil, fmt.Errorf("invalid client (client is nil)") - } - // if we have a client, try making the request for the ethernet interfaces - eths, err := client.FetchEthernetInterfaces(opts...) + eths, err := smdClient.FetchEthernetInterfaces(params.Verbose) if err != nil { return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err) } @@ -51,15 +44,8 @@ func (g *Warewulf) Generate(config *configurator.Config, opts ...util.Option) (F 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("template: \n%s\n ethernet interfaces found: %v\n", strings.Join(target.TemplatePaths, "\n\t"), len(eths)) - } - } - // fetch redfish endpoints and handle errors - eps, err := client.FetchRedfishEndpoints(opts...) + eps, err := smdClient.FetchRedfishEndpoints(params.Verbose) if err != nil { return nil, fmt.Errorf("failed to fetch redfish endpoints: %v", err) } @@ -67,31 +53,21 @@ func (g *Warewulf) Generate(config *configurator.Config, opts ...util.Option) (F return nil, fmt.Errorf("no redfish endpoints found") } - // format output for template substitution - nodeEntries := "" - - // load files and templates and copy to outputs - files, err := LoadFiles(target.FilePaths...) - if err != nil { - return nil, fmt.Errorf("failed to load files: %v", err) - } - templates, err := ApplyTemplateFromFiles(Mappings{ + templates, err := ApplyTemplates(Mappings{ "node_entries": nodeEntries, - }, target.TemplatePaths...) + }, params.Templates) if err != nil { return nil, fmt.Errorf("failed to load templates: %v", err) } - maps.Copy(outputs, files) + maps.Copy(outputs, params.Files) maps.Copy(outputs, templates) // print message if verbose param is found - if verbose, ok := params["verbose"].(bool); ok { - if verbose { - fmt.Printf("templates and files loaded: \n") - for path, _ := range outputs { - fmt.Printf("\t%s", path) - } + if params.Verbose { + fmt.Printf("templates and files loaded: \n") + for path, _ := range outputs { + fmt.Printf("\t%s", path) } }