Merge pull request #6 from OpenCHAMI/refactor

Refactored more code with general improvements
This commit is contained in:
David Allen 2024-07-03 11:43:30 -06:00 committed by GitHub
commit 7a234c1e16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1188 additions and 456 deletions

5
.gitignore vendored
View file

@ -1,4 +1,7 @@
**configurator** **configurator**
**.yaml **.yaml
**.yml **.yml
**.so **.so
**.conf
**.ignore
**.tar.gz

View file

@ -4,6 +4,7 @@ all: plugins exe
# build the main executable to make configs # build the main executable to make configs
main: exe main: exe
driver: exe
exe: exe:
go build --tags=all -o configurator go build --tags=all -o configurator
@ -12,6 +13,15 @@ plugins:
mkdir -p lib mkdir -p lib
go build -buildmode=plugin -o lib/conman.so internal/generator/plugins/conman/conman.go go build -buildmode=plugin -o lib/conman.so internal/generator/plugins/conman/conman.go
go build -buildmode=plugin -o lib/coredhcp.so internal/generator/plugins/coredhcp/coredhcp.go go build -buildmode=plugin -o lib/coredhcp.so internal/generator/plugins/coredhcp/coredhcp.go
go build -buildmode=plugin -o lib/dhcpd.so internal/generator/plugins/dhcpd/dhcpd.go
go build -buildmode=plugin -o lib/dnsmasq.so internal/generator/plugins/dnsmasq/dnsmasq.go go build -buildmode=plugin -o lib/dnsmasq.so internal/generator/plugins/dnsmasq/dnsmasq.go
go build -buildmode=plugin -o lib/hostfile.so internal/generator/plugins/hostfile/hostfile.go
go build -buildmode=plugin -o lib/powerman.so internal/generator/plugins/powerman/powerman.go go build -buildmode=plugin -o lib/powerman.so internal/generator/plugins/powerman/powerman.go
go build -buildmode=plugin -o lib/syslog.so internal/generator/plugins/syslog/syslog.go go build -buildmode=plugin -o lib/syslog.so internal/generator/plugins/syslog/syslog.go
go build -buildmode=plugin -o lib/warewulf.so internal/generator/plugins/warewulf/warewulf.go
# remove executable and all plugins
clean:
rm configurator
rm lib/*

View file

@ -1,6 +1,6 @@
# OpenCHAMI Configurator # OpenCHAMI Configurator
The `configurator` (portmanteau of config + generator) is a tool that fetchs data from an instance of [SMD](https://github.com/OpenCHAMI/smd) to generate commonly used config files. The tool is also capable of some templating using the Jinja 2 syntax with generator plugins. The `configurator` (portmanteau of config + generator) is an extensible tool that fetchs data from an instance of [SMD](https://github.com/OpenCHAMI/smd) to generate commonly used config files based on Jinja 2 template files. The tool and generator plugins are written in Go and plugins can be written by following the ["Creating Generator Plugins"](#creating-generator-plugins) section of this README.
## Building and Usage ## Building and Usage
@ -32,29 +32,33 @@ These commands will build the default plugins and store them in the "lib" direct
This will generate a new `dnsmasq` config file based on the Jinja 2 template specified in the config file for "dnsmasq". The `--target` flag specifies the type of config file to generate by its name (see the [`Creating Generator Plugins`](#creating-generator-plugins) section for details). The `configurator` tool requires a valid access token when making requests to an instance of SMD that has protected routes. This will generate a new `dnsmasq` config file based on the Jinja 2 template specified in the config file for "dnsmasq". The `--target` flag specifies the type of config file to generate by its name (see the [`Creating Generator Plugins`](#creating-generator-plugins) section for details). The `configurator` tool requires a valid access token when making requests to an instance of SMD that has protected routes.
The tool can also run as a microservice: The tool can also run as a service to generate files for clients:
```bash ```bash
./configurator serve --config config.yaml ./configurator serve --config config.yaml
``` ```
Once the server is up and listening for HTTP requests, you can try making a request to it with curl: Once the server is up and listening for HTTP requests, you can try making a request to it with `curl` or `configurator fetch`. Both commands below are essentially equivalent:
```bash ```bash
curl http://127.0.0.1:3334/target?type=dhcp&template=dnsmasq curl http://127.0.0.1:3334/generate?target=dnsmasq -H "Authorization: Bearer $ACCESS_TOKEN"
# ...or...
./configurator fetch --target dnsmasq --host http://127.0.0.1 --port 3334
``` ```
This will do the same thing as the `generate` subcommand, but remotely. This will do the same thing as the `generate` subcommand, but remotely. The access token is only required if the `CONFIGURATOR_JWKS_URL` environment variable is set. The `ACCESS_TOKEN` environment variable passed to `curl` and it's corresponding CLI argument both expects a token as a JWT.
### Creating Generator Plugins ### Creating Generator Plugins
The `configurator` uses generator plugins to define how config files are generated using a `Generator` interface. The interface is defined like so: The `configurator` uses generator plugins to define how config files are generated using a `Generator` interface. The interface is defined like so:
```go ```go
type Files = map[string][]byte
type Generator interface { type Generator interface {
GetName() string GetName() string
GetGroups() []string GetVersion() string
Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) GetDescription() string
Generate(config *configurator.Config, opts ...util.Option) (Files, error)
} }
``` ```
@ -66,14 +70,20 @@ package main
type MyGenerator struct {} type MyGenerator struct {}
func (g *MyGenerator) GetName() string { func (g *MyGenerator) GetName() string {
return "my-generator" // just an example...this can be done however you want
pluginInfo := LoadFromFile("path/to/plugin/info.json")
return pluginInfo["name"]
} }
func (g *MyGenerator) GetGroups() []string { func (g *MyGenerator) GetVersion() string {
return []string{ "my-generator" } return "v1.0.0"
} }
func (g *MyGenerator) Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) { func (g *MyGenerator) GetDescription() string {
return "This is an example plugin."
}
func (g *MyGenerator) Generate(config *configurator.Config, opts ...util.Option) (map[string][]byte, error) {
// do config generation stuff here... // do config generation stuff here...
var ( var (
params = generator.GetParams(opts...) params = generator.GetParams(opts...)
@ -85,7 +95,7 @@ func (g *MyGenerator) Generate(config *configurator.Config, opts ...util.Option)
// ... blah, blah, blah, format output, and so on... // ... blah, blah, blah, format output, and so on...
} }
// apply the template and get substituted output as byte array // apply the substitutions to Jinja template and return output as byte array
return generator.ApplyTemplate(path, generator.Mappings{ return generator.ApplyTemplate(path, generator.Mappings{
"hosts": output, "hosts": output,
}) })
@ -101,38 +111,41 @@ Finally, build the plugin and put it somewhere specified by `plugins` in your co
go build -buildmode=plugin -o lib/mygenerator.so path/to/mygenerator.go go build -buildmode=plugin -o lib/mygenerator.so path/to/mygenerator.go
``` ```
Now your plugin should be available to use with the `configurator` main driver. Now your plugin should be available to use with the `configurator` main driver. If you get an error about not loading the correct symbol type, make sure that you generator function definitions match the `Generator` interface exactly.
## Configuration ## Configuration
Here is an example config file to start using configurator: Here is an example config file to start using configurator:
```yaml ```yaml
server: server: # Server-related parameters when using as service
host: 127.0.0.1 host: 127.0.0.1
port: 3334 port: 3334
jwks: jwks: # Set the JWKS uri to protect /generate route
uri: "" uri: ""
retries: 5 retries: 5
smd: smd: . # SMD-related parameters
host: http://127.0.0.1 host: http://127.0.0.1
port: 27779 port: 27779
templates: plugins: # path to plugin directories
dnsmasq: templates/dnsmasq.jinja
coredhcp: templates/coredhcp.jinja
syslog: templates/syslog.jinja
ansible: templates/ansible.jinja
powerman: templates/powerman.jinja
conman: templates/conman.jinja
groups:
warewulf:
- dnsmasq
- syslog
- ansible
- powerman
- conman
plugins:
- "lib/" - "lib/"
targets: # targets to call with --target flag
dnsmasq:
templates:
- templates/dnsmasq.jinja
warewulf:
templates: # files using Jinja templating
- templates/warewulf/vnfs/dhcpd-template.jinja
- templates/warewulf/vnfs/dnsmasq-template.jinja
files: # files to be copied without templating
- templates/warewulf/defaults/provision.jinja
- templates/warewulf/defaults/node.jinja
- templates/warewulf/filesystem/examples/*
- templates/warewulf/vnfs/*
- templates/warewulf/bootstrap.jinja
- templates/warewulf/database.jinja
targets: # additional targets to run
- dnsmasq
``` ```
The `server` section sets the properties for running the `configurator` tool as a service and is not required if you're only using the CLI. Also note that the `jwks-uri` parameter is only needs for protecting endpoints. If it is not set, then the API is entirely public. The `smd` section tells the `configurator` tool where to find SMD to pull state management data used by the internal client. The `templates` section is where the paths are mapped to each generator plugin by its name (see the [`Creating Generator Plugins`](#creating-generator-plugins) section for details). The `plugins` is a list of paths to load generator plugins. The `server` section sets the properties for running the `configurator` tool as a service and is not required if you're only using the CLI. Also note that the `jwks-uri` parameter is only needs for protecting endpoints. If it is not set, then the API is entirely public. The `smd` section tells the `configurator` tool where to find SMD to pull state management data used by the internal client. The `templates` section is where the paths are mapped to each generator plugin by its name (see the [`Creating Generator Plugins`](#creating-generator-plugins) section for details). The `plugins` is a list of paths to load generator plugins.
@ -140,9 +153,10 @@ The `server` section sets the properties for running the `configurator` tool as
## Known Issues ## Known Issues
- Adds a new `OAuthClient` with every token request - Adds a new `OAuthClient` with every token request
- Plugins are being loaded each time a file is generated
## TODO ## TODO
- Add group functionality - Add group functionality to create by files by groups
- Extend SMD client functionality - Extend SMD client functionality (or make extensible?)
- Redo service API with authorization - Handle authentication with `OAuthClient`'s correctly

54
cmd/fetch.go Normal file
View file

@ -0,0 +1,54 @@
//go:build client || all
// +build client all
package cmd
import (
"fmt"
"net/http"
"github.com/OpenCHAMI/configurator/internal/util"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
remoteHost string
remotePort int
)
var fetchCmd = &cobra.Command{
Use: "fetch",
Short: "Fetch a config file from a remote instance of configurator",
Run: func(cmd *cobra.Command, args []string) {
// make sure a host is set
if remoteHost == "" {
logrus.Errorf("no '--host' argument set")
return
}
for _, target := range targets {
// make a request for each target
url := fmt.Sprintf("%s:%d/generate?target=%s", remoteHost, remotePort, target)
res, body, err := util.MakeRequest(url, http.MethodGet, nil, nil)
if err != nil {
logrus.Errorf("failed to make request: %v", err)
return
}
if res != nil {
if res.StatusCode == http.StatusOK {
fmt.Printf("%s\n", string(body))
}
}
}
},
}
func init() {
fetchCmd.Flags().StringVar(&remoteHost, "host", "", "set the remote configurator host")
fetchCmd.Flags().IntVar(&remotePort, "port", 3334, "set the remote configurator port")
fetchCmd.Flags().StringSliceVar(&targets, "target", nil, "set the target configs to make")
fetchCmd.Flags().StringVarP(&outputPath, "output", "o", "", "set the output path for config targets")
rootCmd.AddCommand(fetchCmd)
}

View file

@ -6,66 +6,25 @@ package cmd
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps"
"os" "os"
"path/filepath" "path/filepath"
configurator "github.com/OpenCHAMI/configurator/internal" configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/generator" "github.com/OpenCHAMI/configurator/internal/generator"
"github.com/sirupsen/logrus" "github.com/OpenCHAMI/configurator/internal/util"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var ( var (
tokenFetchRetries int tokenFetchRetries int
pluginPaths []string pluginPaths []string
cacertPath string
) )
var generateCmd = &cobra.Command{ var generateCmd = &cobra.Command{
Use: "generate", Use: "generate",
Short: "Generate a config file from state management", Short: "Generate a config file from state management",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// 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 // make sure that we have a token present before trying to make request
if config.AccessToken == "" { if config.AccessToken == "" {
// TODO: make request to check if request will need token // TODO: make request to check if request will need token
@ -82,126 +41,106 @@ var generateCmd = &cobra.Command{
} }
} }
if targets == nil { // use cert path from cobra if empty
logrus.Errorf("no target supplied (--target type:template)") // TODO: this needs to be checked for the correct desired behavior
} else { if config.CertPath == "" {
// if we have more than one target and output is set, create configs in directory config.CertPath = cacertPath
targetCount := len(targets)
if outputPath != "" && targetCount > 1 {
err := os.MkdirAll(outputPath, 0o755)
if err != nil {
logrus.Errorf("failed to make output directory: %v", err)
return
}
}
for _, target := range targets {
// split the target and type
// 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[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]
// }
// 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 _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 == "ansible" {
// } 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", 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
} }
// use config plugins if none supplied via CLI
if len(pluginPaths) <= 0 {
pluginPaths = append(pluginPaths, config.PluginDirs...)
}
// 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))
}
RunTargets(targets...)
}, },
} }
func RunTargets(config *configurator.Config, targets ...string) {
// generate config with each supplied target
for _, target := range targets {
params := generator.Params{
Args: args,
PluginPaths: pluginPaths,
Target: target,
Verbose: verbose,
}
outputBytes, err := generator.Generate(&config, params)
if err != nil {
fmt.Printf("failed to generate config: %v\n", err)
os.Exit(1)
}
outputMap := util.ConvertMapOutput(outputBytes)
// if we have more than one target and output is set, create configs in directory
var (
targetCount = len(targets)
templateCount = len(outputMap)
)
if outputPath == "" {
// write only to stdout by default
if len(outputMap) == 1 {
for _, contents := range outputMap {
fmt.Printf("%s\n", string(contents))
}
} else {
for path, contents := range outputMap {
fmt.Printf("-- file: %s, size: %d B\n%s\n", path, len(contents), string(contents))
}
}
} else if outputPath != "" && targetCount == 1 && templateCount == 1 {
// write just a single file using provided name
for _, contents := range outputBytes {
err := os.WriteFile(outputPath, contents, 0o644)
if err != nil {
fmt.Printf("failed to write config to file: %v", err)
os.Exit(1)
}
fmt.Printf("wrote file to '%s'\n", outputPath)
}
} else if outputPath != "" && targetCount > 1 || templateCount > 1 {
// write multiple files in directory using template name
err := os.MkdirAll(filepath.Clean(outputPath), 0o755)
if err != nil {
fmt.Printf("failed to make output directory: %v\n", err)
os.Exit(1)
}
for path, contents := range outputBytes {
filename := filepath.Base(path)
cleanPath := fmt.Sprintf("%s/%s", filepath.Clean(outputPath), filename)
err := os.WriteFile(cleanPath, contents, 0o755)
if err != nil {
fmt.Printf("failed to write config to file: %v\n", err)
os.Exit(1)
}
fmt.Printf("wrote file to '%s'\n", cleanPath)
}
}
// remove any targets that are the same as current to prevent infinite loop
nextTargets := util.CopyIf(config.Targets[targets].Targets, func(t T) bool { return t != target })
// ...then, run any other targets that the current target has
RunTargets(config, nextTargets...)
}
}
func init() { func init() {
generateCmd.Flags().StringSliceVar(&targets, "target", nil, "set the target configs to make") generateCmd.Flags().StringSliceVar(&targets, "target", []string{}, "set the target configs to make")
generateCmd.Flags().StringSliceVar(&pluginPaths, "plugin", nil, "set the generator plugins directory path to shared libraries") generateCmd.Flags().StringSliceVar(&pluginPaths, "plugins", []string{}, "set the generator plugins directory path")
generateCmd.Flags().StringVarP(&outputPath, "output", "o", "", "set the output path for config targets") generateCmd.Flags().StringVarP(&outputPath, "output", "o", "", "set the output path for config targets")
generateCmd.Flags().StringVar(&cacertPath, "ca-cert", "", "path to CA cert. (defaults to system CAs)")
generateCmd.Flags().IntVar(&tokenFetchRetries, "fetch-retries", 5, "set the number of retries to fetch an access token") generateCmd.Flags().IntVar(&tokenFetchRetries, "fetch-retries", 5, "set the number of retries to fetch an access token")
rootCmd.AddCommand(generateCmd) rootCmd.AddCommand(generateCmd)

63
cmd/inspect.go Normal file
View file

@ -0,0 +1,63 @@
package cmd
import (
"fmt"
"maps"
"strings"
"github.com/OpenCHAMI/configurator/internal/generator"
"github.com/spf13/cobra"
)
var (
pluginDirs []string
generators map[string]generator.Generator
)
var inspectCmd = &cobra.Command{
Use: "inspect",
Short: "Inspect generator plugin information",
Run: func(cmd *cobra.Command, args []string) {
// load specific plugins from positional args
generators = make(map[string]generator.Generator)
for _, path := range args {
gen, err := generator.LoadPlugin(path)
if err != nil {
fmt.Printf("failed to load plugin at path '%s': %v\n", path, err)
continue
}
generators[path] = gen
}
// load plugins and print all plugin details
if len(pluginDirs) > 0 {
} else {
for _, pluginDir := range config.PluginDirs {
gens, err := generator.LoadPlugins(pluginDir)
if err != nil {
fmt.Printf("failed to load plugin: %v\n", err)
continue
}
maps.Copy(generators, gens)
}
}
// print all generator information
if len(generators) > 0 {
o := ""
for _, g := range generators {
o += fmt.Sprintf("- Name: %s\n", g.GetName())
o += fmt.Sprintf(" Version: %s\n", g.GetVersion())
o += fmt.Sprintf(" Description: %s\n", g.GetDescription())
o += "\n"
}
o = strings.TrimRight(o, "\n")
fmt.Printf("%s", o)
}
},
}
func init() {
rootCmd.AddCommand(inspectCmd)
}

View file

@ -55,4 +55,14 @@ func initConfig() {
} else { } else {
config = configurator.NewConfig() config = configurator.NewConfig()
} }
//
// set environment variables to override config values
//
// set the JWKS url if we find the CONFIGURATOR_JWKS_URL environment variable
jwksUrl := os.Getenv("CONFIGURATOR_JWKS_URL")
if jwksUrl != "" {
config.Server.Jwks.Uri = jwksUrl
}
} }

View file

@ -4,13 +4,14 @@
package cmd package cmd
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"github.com/OpenCHAMI/configurator/internal/generator"
"github.com/OpenCHAMI/configurator/internal/server" "github.com/OpenCHAMI/configurator/internal/server"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -18,13 +19,58 @@ var serveCmd = &cobra.Command{
Use: "serve", Use: "serve",
Short: "Start configurator as a server and listen for requests", Short: "Start configurator as a server and listen for requests",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// 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 verbose {
fmt.Printf("No token found. Attempting to generate config without one...\n")
}
}
}
// use config plugins if none supplied via CLI
if len(pluginPaths) <= 0 {
pluginPaths = append(pluginPaths, config.PluginDirs...)
}
// 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))
}
// set up the routes and start the server // set up the routes and start the server
server := server.New() server := server.Server{
err := server.Start(&config) Config: &config,
Server: &http.Server{
Addr: fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port),
},
Jwks: server.Jwks{
Uri: config.Server.Jwks.Uri,
Retries: config.Server.Jwks.Retries,
},
GeneratorParams: generator.Params{
Args: args,
PluginPaths: pluginPaths,
// Target: target, // NOTE: targets are set via HTTP requests (ex: curl http://configurator:3334/generate?target=dnsmasq)
Verbose: verbose,
},
}
err := server.Serve()
if errors.Is(err, http.ErrServerClosed) { if errors.Is(err, http.ErrServerClosed) {
fmt.Printf("Server closed.") fmt.Printf("Server closed.")
} else if err != nil { } else if err != nil {
logrus.Errorf("failed to start server: %v", err) fmt.Errorf("failed to start server: %v", err)
os.Exit(1) os.Exit(1)
} }
}, },
@ -33,7 +79,8 @@ var serveCmd = &cobra.Command{
func init() { func init() {
serveCmd.Flags().StringVar(&config.Server.Host, "host", config.Server.Host, "set the server host") serveCmd.Flags().StringVar(&config.Server.Host, "host", config.Server.Host, "set the server host")
serveCmd.Flags().IntVar(&config.Server.Port, "port", config.Server.Port, "set the server port") serveCmd.Flags().IntVar(&config.Server.Port, "port", config.Server.Port, "set the server port")
serveCmd.Flags().StringVar(&config.Options.JwksUri, "jwks-uri", config.Options.JwksUri, "set the JWKS url to fetch public key") serveCmd.Flags().StringSliceVar(&pluginPaths, "plugins", nil, "set the generator plugins directory path")
serveCmd.Flags().IntVar(&config.Options.JwksRetries, "jwks-fetch-retries", config.Options.JwksRetries, "set the JWKS fetch retry count") serveCmd.Flags().StringVar(&config.Server.Jwks.Uri, "jwks-uri", config.Server.Jwks.Uri, "set the JWKS url to fetch public key")
serveCmd.Flags().IntVar(&config.Server.Jwks.Retries, "jwks-fetch-retries", config.Server.Jwks.Retries, "set the JWKS fetch retry count")
rootCmd.AddCommand(serveCmd) rootCmd.AddCommand(serveCmd)
} }

View file

@ -0,0 +1,20 @@
#
# This file was auto-generated by the OpenCHAMI "configurator" tool using the "{{name}}" plugin.
#
# Source code: https://github.com/OpenCHAMI/configurator
# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins
#
SERVER keepalive=ON
SERVER logdir="/var/log/conman"
SERVER logfile="/var/log/conman.log"
SERVER loopback=ON
SERVER pidfile="/var/run/conman.pid"
SERVER resetcmd="/usr/bin/powerman -0 \%N; sleep 5; /usr/bin/powerman -1 \%N"
SERVER tcpwrappers=ON
#SERVER timestamp=1h
GLOBAL seropts="115200,8n1"
GLOBAL log="/var/log/conman/console.\%N"
GLOBAL logopts="sanitize,timestamp"
{{ consoles }}

View file

@ -0,0 +1,48 @@
#
# This file was auto-generated by the OpenCHAMI "configurator" tool using the "{{name}}" plugin.
#
# Source code: https://github.com/OpenCHAMI/configurator
# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins
#
allow booting;
allow bootp;
ddns-update-style interim;
authoritative;
option space ipxe;
# Tell iPXE to not wait for ProxyDHCP requests to speed up boot.
option ipxe.no-pxedhcp code 176 = unsigned integer 8;
option ipxe.no-pxedhcp 1;
option architecture-type code 93 = unsigned integer 16;
if exists user-class and option user-class = "iPXE" {
filename "http://%{IPADDR}/WW/ipxe/cfg/${mac}";
} else {
if option architecture-type = 00:0B {
filename "/warewulf/ipxe/bin-arm64-efi/snp.efi";
} elsif option architecture-type = 00:0A {
filename "/warewulf/ipxe/bin-arm32-efi/placeholder.efi";
} elsif option architecture-type = 00:09 {
filename "/warewulf/ipxe/bin-x86_64-efi/snp.efi";
} elsif option architecture-type = 00:07 {
filename "/warewulf/ipxe/bin-x86_64-efi/snp.efi";
} elsif option architecture-type = 00:06 {
filename "/warewulf/ipxe/bin-i386-efi/snp.efi";
} elsif option architecture-type = 00:00 {
filename "/warewulf/ipxe/bin-i386-pcbios/undionly.kpxe";
}
}
subnet %{NETWORK} netmask %{NETMASK} {
not authoritative;
# option interface-mtu 9000;
option subnet-mask %{NETMASK};
}
# Compute Nodes (WIP - see the dhcpd generator plugin)
{{ compute_nodes }}
# Node entries will follow below
{{ node_entries }}

View file

@ -0,0 +1,7 @@
#
# This file was auto-generated by the OpenCHAMI "configurator" tool using the "{{name}}" plugin.
#
# Source code: https://github.com/OpenCHAMI/configurator
# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins
#
{{ output }}

View file

@ -0,0 +1,12 @@
#
# Ansible managed
#
include "/etc/powerman/ipmipower.dev"
include "/etc/powerman/ipmi.dev"
# list of devices
{{ devices }}
# create nodes based on found nodes in hostfile
{{ nodes }}

View file

@ -2,16 +2,21 @@ package configurator
import ( import (
"bytes" "bytes"
"crypto/tls"
"crypto/x509"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"os"
"time"
"github.com/OpenCHAMI/configurator/internal/util" "github.com/OpenCHAMI/configurator/internal/util"
) )
type SmdClient struct { type SmdClient struct {
http.Client http.Client `json:"-"`
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
AccessToken string `yaml:"access-token"` AccessToken string `yaml:"access-token"`
@ -19,8 +24,63 @@ type SmdClient struct {
type Params = map[string]any type Params = map[string]any
type Option func(Params) type Option func(Params)
type ClientOption func(*SmdClient)
func WithVerbose() Option { func NewSmdClient(opts ...ClientOption) SmdClient {
client := SmdClient{}
for _, opt := range opts {
opt(&client)
}
return client
}
func WithHost(host string) ClientOption {
return func(c *SmdClient) {
c.Host = host
}
}
func WithPort(port int) ClientOption {
return func(c *SmdClient) {
c.Port = port
}
}
func WithAccessToken(token string) ClientOption {
return func(c *SmdClient) {
c.AccessToken = token
}
}
func WithCertPool(certPool *x509.CertPool) ClientOption {
return func(c *SmdClient) {
c.Client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
InsecureSkipVerify: true,
},
DisableKeepAlives: true,
Dial: (&net.Dialer{
Timeout: 120 * time.Second,
KeepAlive: 120 * time.Second,
}).Dial,
TLSHandshakeTimeout: 120 * time.Second,
ResponseHeaderTimeout: 120 * time.Second,
}
}
}
func WithSecureTLS(certPath string) ClientOption {
if certPath == "" {
return func(sc *SmdClient) {}
}
cacert, _ := os.ReadFile(certPath)
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(cacert)
return WithCertPool(certPool)
}
func WithVerbosity() Option {
return func(p util.Params) { return func(p util.Params) {
p["verbose"] = true p["verbose"] = true
} }
@ -35,6 +95,11 @@ func NewParams() Params {
// Fetch the ethernet interfaces from SMD service using its API. An access token may be required if the SMD // 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. // service SMD_JWKS_URL envirnoment variable is set.
func (client *SmdClient) FetchEthernetInterfaces(opts ...util.Option) ([]EthernetInterface, error) { 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 // make request to SMD endpoint
b, err := client.makeRequest("/Inventory/EthernetInterfaces") b, err := client.makeRequest("/Inventory/EthernetInterfaces")
if err != nil { if err != nil {
@ -42,16 +107,14 @@ func (client *SmdClient) FetchEthernetInterfaces(opts ...util.Option) ([]Etherne
} }
// unmarshal response body JSON and extract in object // unmarshal response body JSON and extract in object
eths := []EthernetInterface{} // []map[string]any{}
err = json.Unmarshal(b, &eths) err = json.Unmarshal(b, &eths)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %v", err) return nil, fmt.Errorf("failed to unmarshal response: %v", err)
} }
// print what we got if verbose is set // print what we got if verbose is set
params := util.GetParams(opts...) if verbose != nil {
if verbose, ok := params["verbose"].(bool); ok { if *verbose {
if verbose {
fmt.Printf("Ethernet Interfaces: %v\n", string(b)) fmt.Printf("Ethernet Interfaces: %v\n", string(b))
} }
} }
@ -62,23 +125,40 @@ 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 // 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. // service SMD_JWKS_URL envirnoment variable is set.
func (client *SmdClient) FetchComponents(opts ...util.Option) ([]Component, error) { 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 // make request to SMD endpoint
b, err := client.makeRequest("/State/Components") b, err := client.makeRequest("/State/Components")
if err != nil { 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)
}
// make sure our response is actually JSON
if !json.Valid(b) {
return nil, fmt.Errorf("expected valid JSON response: %v", string(b))
} }
// unmarshal response body JSON and extract in object // unmarshal response body JSON and extract in object
comps := []Component{} var tmp map[string]any
err = json.Unmarshal(b, &tmp)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
}
b, err = json.Marshal(tmp["RedfishEndpoints"].([]any))
if err != nil {
return nil, fmt.Errorf("failed to marshal JSON: %v", err)
}
err = json.Unmarshal(b, &comps) err = json.Unmarshal(b, &comps)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %v", err) return nil, fmt.Errorf("failed to unmarshal response: %v", err)
} }
// print what we got if verbose is set // print what we got if verbose is set
params := util.GetParams(opts...) if verbose != nil {
if verbose, ok := params["verbose"].(bool); ok { if *verbose {
if verbose {
fmt.Printf("Components: %v\n", string(b)) fmt.Printf("Components: %v\n", string(b))
} }
} }
@ -86,6 +166,44 @@ func (client *SmdClient) FetchComponents(opts ...util.Option) ([]Component, erro
return comps, nil return comps, nil
} }
func (client *SmdClient) FetchRedfishEndpoints(opts ...util.Option) ([]RedfishEndpoint, error) {
var (
params = util.GetParams(opts...)
verbose = util.Get[bool](params, "verbose")
eps = []RedfishEndpoint{}
)
b, err := client.makeRequest("/Inventory/RedfishEndpoints")
if err != nil {
return nil, fmt.Errorf("failed to make HTTP resquest: %v", err)
}
if !json.Valid(b) {
return nil, fmt.Errorf("expected valid JSON response: %v", string(b))
}
var tmp map[string]any
err = json.Unmarshal(b, &tmp)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
}
b, err = json.Marshal(tmp["RedfishEndpoints"].([]any))
if err != nil {
return nil, fmt.Errorf("failed to marshal JSON: %v", err)
}
err = json.Unmarshal(b, &eps)
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 eps, nil
}
func (client *SmdClient) makeRequest(endpoint string) ([]byte, error) { func (client *SmdClient) makeRequest(endpoint string) ([]byte, error) {
if client == nil { if client == nil {
return nil, fmt.Errorf("client is nil") return nil, fmt.Errorf("client is nil")

View file

@ -10,6 +10,12 @@ import (
type Options struct{} type Options struct{}
type Target struct {
Templates []string `yaml:"templates,omitempty"`
FilePaths []string `yaml:"files,omitempty"`
RunTargets []string `yaml:"targets,omitempty"`
}
type Jwks struct { type Jwks struct {
Uri string `yaml:"uri"` Uri string `yaml:"uri"`
Retries int `yaml:"retries"` Retries int `yaml:"retries"`
@ -22,13 +28,14 @@ type Server struct {
} }
type Config struct { type Config struct {
Version string `yaml:"version"` Version string `yaml:"version"`
Server Server `yaml:"server"` Server Server `yaml:"server"`
SmdClient SmdClient `yaml:"smd"` SmdClient SmdClient `yaml:"smd"`
AccessToken string `yaml:"access-token"` AccessToken string `yaml:"access-token"`
TemplatePaths map[string]string `yaml:"templates"` Targets map[string]Target `yaml:"targets"`
Plugins []string `yaml:"plugins"` PluginDirs []string `yaml:"plugins"`
Options Options `yaml:"options"` CertPath string `yaml:"ca-cert"`
Options Options `yaml:"options"`
} }
func NewConfig() Config { func NewConfig() Config {
@ -38,14 +45,22 @@ func NewConfig() Config {
Host: "http://127.0.0.1", Host: "http://127.0.0.1",
Port: 27779, Port: 27779,
}, },
TemplatePaths: map[string]string{ Targets: map[string]Target{
"dnsmasq": "templates/dnsmasq.jinja", "dnsmasq": Target{
"syslog": "templates/syslog.jinja", Templates: []string{},
"ansible": "templates/ansible.jinja", },
"powerman": "templates/powerman.jinja", "conman": Target{
"conman": "templates/conman.jinja", Templates: []string{},
},
"warewulf": Target{
Templates: []string{
"templates/warewulf/defaults/node.jinja",
"templates/warewulf/defaults/provision.jinja",
},
},
}, },
Plugins: []string{},
PluginDirs: []string{},
Server: Server{ Server: Server{
Host: "127.0.0.1", Host: "127.0.0.1",
Port: 3334, Port: 3334,

View file

@ -1,56 +0,0 @@
version: "0.0.1"
server:
host: "127.0.0.1"
port: 3333
callback: "/oidc/callback"
providers:
facebook: "http://facebook.com"
forgejo: "http://git.towk.local:3000"
gitlab: "https://gitlab.newmexicoconsortium.org"
github: "https://github.com"
authentication:
clients:
- id: "7527e7b4-c96a-4df0-8fc5-00fde18bb65d"
secret: "gto_cc5uvpb5lsdczkwnbarvwmbpv5kcjwg7nhbc75zt65yrfh2ldenq"
name: "forgejo"
issuer: "http://git.towk.local:3000"
scope:
- "openid"
- "profile"
- "read"
- "email"
redirect-uris:
- "http://127.0.0.1:3333/oidc/callback"
- id: "7c0fab1153674a258a705976fcb9468350df3addd91de4ec622fc9ed24bfbcdd"
secret: "a9a8bc55b0cd99236756093adc00ab17855fa507ce106b8038e7f9390ef2ad99"
name: "gitlab"
issuer: "http://gitlab.newmexicoconsortium.org"
scope:
- "openid"
- "profile"
- "email"
redirect-uris:
- "http://127.0.0.1:3333/oidc/callback"
flows:
authorization-code:
state: ""
client-credentials:
authorization:
urls:
#identities: http://127.0.0.1:4434/admin/identities
trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers
login: http://127.0.0.1:4433/self-service/login/api
clients: http://127.0.0.1:4445/admin/clients
authorize: http://127.0.0.1:4444/oauth2/auth
register: http://127.0.0.1:4444/oauth2/register
token: http://127.0.0.1:4444/oauth2/token
options:
decode-id-token: true
decode-access-token: true
run-once: true
open-browser: false

View file

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

View file

@ -3,20 +3,32 @@ package generator
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"maps"
"os" "os"
"path/filepath"
"plugin" "plugin"
configurator "github.com/OpenCHAMI/configurator/internal" configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/util" "github.com/OpenCHAMI/configurator/internal/util"
"github.com/nikolalohinski/gonja/v2" "github.com/nikolalohinski/gonja/v2"
"github.com/nikolalohinski/gonja/v2/exec" "github.com/nikolalohinski/gonja/v2/exec"
"github.com/sirupsen/logrus"
) )
type Mappings = map[string]any type Mappings = map[string]any
type Files = map[string][]byte
type Generator interface { type Generator interface {
GetName() string GetName() string
GetGroups() []string GetVersion() string
Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) GetDescription() string
Generate(config *configurator.Config, opts ...util.Option) (Files, error)
}
type Params struct {
Args []string
PluginPaths []string
Target string
Verbose bool
} }
func LoadPlugin(path string) (Generator, error) { func LoadPlugin(path string) (Generator, error) {
@ -25,14 +37,16 @@ func LoadPlugin(path string) (Generator, error) {
return nil, fmt.Errorf("failed to load plugin: %v", err) return nil, fmt.Errorf("failed to load plugin: %v", err)
} }
// load the "Generator" symbol from plugin
symbol, err := p.Lookup("Generator") symbol, err := p.Lookup("Generator")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to look up symbol: %v", err) return nil, fmt.Errorf("failed to look up symbol at path '%s': %v", path, err)
} }
// assert that the plugin loaded has a valid generator
gen, ok := symbol.(Generator) gen, ok := symbol.(Generator)
if !ok { if !ok {
return nil, fmt.Errorf("failed to load the correct symbol type") return nil, fmt.Errorf("failed to load the correct symbol type at path '%s'", path)
} }
return gen, nil return gen, nil
} }
@ -45,53 +59,33 @@ func LoadPlugins(dirpath string, opts ...util.Option) (map[string]Generator, err
) )
items, _ := os.ReadDir(dirpath) 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 { for _, item := range items {
if item.IsDir() { if item.IsDir() {
subitems, _ := os.ReadDir(item.Name()) subitems, _ := os.ReadDir(item.Name())
for _, subitem := range subitems { for _, subitem := range subitems {
if !subitem.IsDir() { if !subitem.IsDir() {
gen, err := LoadGenerator(subitem.Name()) gen, err := LoadPlugin(subitem.Name())
if err != nil { if err != nil {
fmt.Printf("failed to load generator in directory '%s': %v\n", item.Name(), err) fmt.Printf("failed to load generator in directory '%s': %v\n", item.Name(), err)
continue continue
} }
if verbose, ok := params["verbose"].(bool); ok { if verbose, ok := params["verbose"].(bool); ok {
if verbose { if verbose {
fmt.Printf("found plugin '%s'\n", item.Name()) fmt.Printf("-- found plugin '%s'\n", item.Name())
} }
} }
gens[gen.GetName()] = gen gens[gen.GetName()] = gen
} }
} }
} else { } else {
gen, err := LoadGenerator(dirpath + item.Name()) gen, err := LoadPlugin(dirpath + item.Name())
if err != nil { if err != nil {
fmt.Printf("failed to load generator: %v\n", err) fmt.Printf("failed to load generator: %v\n", err)
continue continue
} }
if verbose, ok := params["verbose"].(bool); ok { if verbose, ok := params["verbose"].(bool); ok {
if verbose { if verbose {
fmt.Printf("found plugin '%s'\n", dirpath+item.Name()) fmt.Printf("-- found plugin '%s'\n", dirpath+item.Name())
} }
} }
gens[gen.GetName()] = gen gens[gen.GetName()] = gen
@ -101,10 +95,10 @@ func LoadPlugins(dirpath string, opts ...util.Option) (map[string]Generator, err
return gens, nil return gens, nil
} }
func WithTemplate(_template string) util.Option { func WithTarget(target string) util.Option {
return func(p util.Params) { return func(p util.Params) {
if p != nil { if p != nil {
p["template"] = _template p["target"] = target
} }
} }
} }
@ -123,17 +117,19 @@ func WithClient(client configurator.SmdClient) util.Option {
} }
} }
// Syntactic sugar generic function to get parameter from util.Params. func WithOption(key string, value any) util.Option {
func Get[T any](params util.Params, key string) *T { return func(p util.Params) {
if v, ok := params[key].(T); ok { p[key] = value
return &v
} }
return nil
} }
// Helper function to get client in generator plugins. // Helper function to get client in generator plugins.
func GetClient(params util.Params) *configurator.SmdClient { func GetClient(params util.Params) *configurator.SmdClient {
return Get[configurator.SmdClient](params, "client") return util.Get[configurator.SmdClient](params, "client")
}
func GetTarget(config *configurator.Config, key string) configurator.Target {
return config.Targets[key]
} }
func GetParams(opts ...util.Option) util.Params { func GetParams(opts ...util.Option) util.Params {
@ -144,24 +140,108 @@ func GetParams(opts ...util.Option) util.Params {
return params return params
} }
func Generate(g Generator, config *configurator.Config, opts ...util.Option) { func ApplyTemplates(mappings map[string]any, paths ...string) (Files, error) {
g.Generate(config, opts...) var (
} data = exec.NewContext(mappings)
outputs = Files{}
)
func ApplyTemplate(path string, mappings map[string]any) ([]byte, error) { for _, path := range paths {
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)
}
// load jinja template from file // execute/render jinja template
t, err := gonja.FromFile(path) b := bytes.Buffer{}
if err != nil { if err = t.Execute(&b, data); err != nil {
return nil, fmt.Errorf("failed to read template from file: %v", err) return nil, fmt.Errorf("failed to execute: %v", err)
}
outputs[path] = b.Bytes()
} }
// execute/render jinja template return outputs, nil
b := bytes.Buffer{} }
if err = t.Execute(&b, data); err != nil {
return nil, fmt.Errorf("failed to execute: %v", err) func LoadFiles(paths ...string) (Files, error) {
var outputs = Files{}
for _, path := range paths {
expandedPaths, err := filepath.Glob(path)
if err != nil {
return nil, fmt.Errorf("failed to glob path: %v", err)
}
for _, expandedPath := range expandedPaths {
info, err := os.Stat(expandedPath)
if err != nil {
fmt.Println(err)
return nil, fmt.Errorf("failed to stat file or directory: %v", err)
}
// skip any directories found
if info.IsDir() {
continue
}
b, err := os.ReadFile(expandedPath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %v", err)
}
outputs[expandedPath] = b
}
} }
return b.Bytes(), nil return outputs, nil
}
func Generate(config *configurator.Config, params Params) (Files, error) {
// load generator plugins to generate configs or to print
var (
generators = make(map[string]Generator)
client = configurator.NewSmdClient(
configurator.WithHost(config.SmdClient.Host),
configurator.WithPort(config.SmdClient.Port),
configurator.WithAccessToken(config.AccessToken),
configurator.WithSecureTLS(config.CertPath),
)
)
// 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,
WithTarget(gen.GetName()),
WithClient(client),
)
}
return nil, fmt.Errorf("an unknown error has occurred")
} }

View file

@ -1,14 +1,11 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
configurator "github.com/OpenCHAMI/configurator/internal" configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/generator" "github.com/OpenCHAMI/configurator/internal/generator"
"github.com/OpenCHAMI/configurator/internal/util" "github.com/OpenCHAMI/configurator/internal/util"
"github.com/nikolalohinski/gonja/v2"
"github.com/nikolalohinski/gonja/v2/exec"
) )
type Conman struct{} type Conman struct{}
@ -17,30 +14,56 @@ func (g *Conman) GetName() string {
return "conman" return "conman"
} }
func (g *Conman) GetGroups() []string { func (g *Conman) GetVersion() string {
return []string{"conman"} return util.GitCommit()
} }
func (g *Conman) Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) { func (g *Conman) GetDescription() string {
params := generator.GetParams(opts...) return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName())
}
func (g *Conman) GetGroups() []string {
return []string{""}
}
func (g *Conman) Generate(config *configurator.Config, opts ...util.Option) (map[string][]byte, error) {
var ( var (
template = params["template"].(string) params = generator.GetParams(opts...)
path = config.TemplatePaths[template] client = generator.GetClient(params)
targetKey = params["targets"].(string) // required param
target = config.Targets[targetKey]
eps []configurator.RedfishEndpoint = nil
err error = nil
// serverOpts = ""
// globalOpts = ""
consoles = ""
) )
data := exec.NewContext(map[string]any{})
t, err := gonja.FromFile(path) // fetch required data from SMD to create config
if err != nil { if client != nil {
return nil, fmt.Errorf("failed to read template from file: %v", err) eps, err = client.FetchRedfishEndpoints(opts...)
} if err != nil {
output := "# ========== GENERATED BY OCHAMI CONFIGURATOR ==========\n" return nil, fmt.Errorf("failed to fetch redfish endpoints with client: %v", err)
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 // 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 {
consoles += fmt.Sprintf("CONSOLE name=%s dev=ipmi:%s-bmc ipmiopts=U:%s,P:%s,W:solpayloadsize\n", ep.Name, ep.Name, ep.User, ep.Password)
}
consoles += "# ====================================================================="
// apply template substitutions and return output as byte array
return generator.ApplyTemplates(generator.Mappings{
"server_opts": "",
"global_opts": "",
}, target.Templates...)
} }
var Generator Conman var Generator Conman

View file

@ -1,6 +1,8 @@
package main package main
import ( import (
"fmt"
configurator "github.com/OpenCHAMI/configurator/internal" configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/util" "github.com/OpenCHAMI/configurator/internal/util"
) )
@ -11,12 +13,16 @@ func (g *CoreDhcp) GetName() string {
return "coredhcp" return "coredhcp"
} }
func (g *CoreDhcp) GetGroups() []string { func (g *CoreDhcp) GetVersion() string {
return []string{"coredhcp"} return util.GitCommit()
} }
func (g *CoreDhcp) Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) { func (g *CoreDhcp) GetDescription() string {
return nil, nil return fmt.Sprintf("Configurator generator plugin for '%s' to generate config files. This plugin is not complete and still a WIP.", g.GetName())
}
func (g *CoreDhcp) Generate(config *configurator.Config, opts ...util.Option) (map[string][]byte, error) {
return nil, fmt.Errorf("plugin does not implement generation function")
} }
var Generator CoreDhcp var Generator CoreDhcp

View file

@ -0,0 +1,73 @@
package main
import (
"fmt"
configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/generator"
"github.com/OpenCHAMI/configurator/internal/util"
)
type Dhcpd struct{}
func (g *Dhcpd) GetName() string {
return "dhcpd"
}
func (g *Dhcpd) GetVersion() string {
return util.GitCommit()
}
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) (generator.Files, error) {
var (
params = generator.GetParams(opts...)
client = generator.GetClient(params)
targetKey = params["target"].(string)
target = config.Targets[targetKey]
compute_nodes = ""
eths []configurator.EthernetInterface = nil
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: %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")
}
// format output to write to config file
compute_nodes = "# ========== 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])
}
compute_nodes += "# ====================================================================="
if verbose, ok := params["verbose"].(bool); ok {
if verbose {
fmt.Printf("")
}
}
return generator.ApplyTemplates(generator.Mappings{
"compute_nodes": compute_nodes,
"node_entries": "",
}, target.Templates...)
}
var Generator Dhcpd

View file

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"strings"
configurator "github.com/OpenCHAMI/configurator/internal" configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/generator" "github.com/OpenCHAMI/configurator/internal/generator"
@ -10,28 +11,19 @@ import (
type DnsMasq struct{} 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 { func (g *DnsMasq) GetName() string {
return "dnsmasq" return "dnsmasq"
} }
func (g *DnsMasq) GetGroups() []string { func (g *DnsMasq) GetVersion() string {
return []string{"dnsmasq"} return util.GitCommit()
} }
func (g *DnsMasq) Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) { 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) (map[string][]byte, error) {
// make sure we have a valid config first // make sure we have a valid config first
if config == nil { if config == nil {
return nil, fmt.Errorf("invalid config (config is nil)") return nil, fmt.Errorf("invalid config (config is nil)")
@ -39,15 +31,16 @@ func (g *DnsMasq) Generate(config *configurator.Config, opts ...util.Option) ([]
// set all the defaults for variables // set all the defaults for variables
var ( var (
params = generator.GetParams(opts...) params = generator.GetParams(opts...)
template = params["template"].(string) // required param client = generator.GetClient(params)
path = config.TemplatePaths[template] targetKey = params["target"].(string) // required param
eths []configurator.EthernetInterface = nil target = config.Targets[targetKey]
err error = nil eths []configurator.EthernetInterface = nil
err error = nil
) )
// if we have a client, try making the request for the ethernet interfaces // if we have a client, try making the request for the ethernet interfaces
if client, ok := params["client"].(configurator.SmdClient); ok { if client != nil {
eths, err = client.FetchEthernetInterfaces(opts...) eths, err = client.FetchEthernetInterfaces(opts...)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err) return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err)
@ -65,12 +58,12 @@ func (g *DnsMasq) Generate(config *configurator.Config, opts ...util.Option) ([]
// print message if verbose param found // print message if verbose param found
if verbose, ok := params["verbose"].(bool); ok { if verbose, ok := params["verbose"].(bool); ok {
if verbose { if verbose {
fmt.Printf("path: %s\neth count: %v\n", path, len(eths)) fmt.Printf("template: \n%s\nethernet interfaces found: %v\n", strings.Join(target.Templates, "\n\t"), len(eths))
} }
} }
// format output to write to config file // format output to write to config file
output := "# ========== GENERATED BY OCHAMI CONFIGURATOR ==========\n" output := "# ========== DYNAMICALLY GENERATED BY OPENCHAMI CONFIGURATOR ==========\n"
for _, eth := range eths { for _, eth := range eths {
if eth.Type == "NodeBMC" { if eth.Type == "NodeBMC" {
output += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n" output += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n"
@ -78,12 +71,13 @@ func (g *DnsMasq) Generate(config *configurator.Config, opts ...util.Option) ([]
output += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n" output += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n"
} }
} }
output += "# ======================================================" output += "# ====================================================================="
// apply template substitutions and return output as byte array // apply template substitutions and return output as byte array
return generator.ApplyTemplate(path, generator.Mappings{ return generator.ApplyTemplates(generator.Mappings{
"hosts": output, "name": g.GetName(),
}) "output": output,
}, target.Templates...)
} }
var Generator DnsMasq var Generator DnsMasq

View file

@ -0,0 +1,32 @@
package main
import (
"fmt"
configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/generator"
"github.com/OpenCHAMI/configurator/internal/util"
)
type Example struct {
Message string
}
func (g *Example) GetName() string {
return "example"
}
func (g *Example) GetVersion() string {
return util.GitCommit()
}
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.Files, 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.Files{"example": []byte(g.Message)}, nil
}

View file

@ -0,0 +1,28 @@
package main
import (
"fmt"
configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/util"
)
type Hostfile struct{}
func (g *Hostfile) GetName() string {
return "hostfile"
}
func (g *Hostfile) GetVersion() string {
return util.GitCommit()
}
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) (map[string][]byte, error) {
return nil, fmt.Errorf("plugin does not implement generation function")
}
var Generator Hostfile

View file

@ -0,0 +1 @@
package main

View file

@ -1,6 +1,8 @@
package main package main
import ( import (
"fmt"
configurator "github.com/OpenCHAMI/configurator/internal" configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/util" "github.com/OpenCHAMI/configurator/internal/util"
) )
@ -11,12 +13,16 @@ func (g *Powerman) GetName() string {
return "powerman" return "powerman"
} }
func (g *Powerman) GetGroups() []string { func (g *Powerman) GetVersion() string {
return []string{"powerman"} return util.GitCommit()
} }
func (g *Powerman) Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) { func (g *Powerman) GetDescription() string {
return nil, nil return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName())
}
func (g *Powerman) Generate(config *configurator.Config, opts ...util.Option) (map[string][]byte, error) {
return nil, fmt.Errorf("plugin does not implement generation function")
} }
var Generator Powerman var Generator Powerman

View file

@ -1,6 +1,8 @@
package main package main
import ( import (
"fmt"
configurator "github.com/OpenCHAMI/configurator/internal" configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/util" "github.com/OpenCHAMI/configurator/internal/util"
) )
@ -11,12 +13,16 @@ func (g *Syslog) GetName() string {
return "syslog" return "syslog"
} }
func (g *Syslog) GetGroups() []string { func (g *Syslog) GetVersion() string {
return []string{"log"} return util.GitCommit()
} }
func (g *Syslog) Generate(config *configurator.Config, opts ...util.Option) ([]byte, error) { func (g *Syslog) GetDescription() string {
return nil, nil return fmt.Sprintf("Configurator generator plugin for '%s'.", g.GetName())
}
func (g *Syslog) Generate(config *configurator.Config, opts ...util.Option) (map[string][]byte, error) {
return nil, fmt.Errorf("plugin does not implement generation function")
} }
var Generator Syslog var Generator Syslog

View file

@ -1,12 +1,13 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"maps"
"strings"
configurator "github.com/OpenCHAMI/configurator/internal" configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/nikolalohinski/gonja/v2" "github.com/OpenCHAMI/configurator/internal/generator"
"github.com/nikolalohinski/gonja/v2/exec" "github.com/OpenCHAMI/configurator/internal/util"
) )
type Warewulf struct{} type Warewulf struct{}
@ -15,30 +16,87 @@ func (g *Warewulf) GetName() string {
return "warewulf" return "warewulf"
} }
func (g *Warewulf) GetGroups() []string { func (g *Warewulf) GetVersion() string {
return []string{"warewulf"} return util.GitCommit()
} }
func (g *Warewulf) Generate(config *configurator.Config, template string) ([]byte, error) { func (g *Warewulf) GetDescription() string {
return "Configurator generator plugin for 'warewulf' config files."
}
func (g *Warewulf) Generate(config *configurator.Config, opts ...util.Option) (generator.Files, error) {
var ( var (
path = config.TemplatePaths[template] params = generator.GetParams(opts...)
client = generator.GetClient(params)
targetKey = params["target"].(string)
target = config.Targets[targetKey]
outputs = make(generator.Files, len(target.FilePaths)+len(target.Templates))
) )
t, err := gonja.FromFile(path) // check if our client is included and is valid
if err != nil { if client == nil {
return nil, fmt.Errorf("failed to read template from file: %v", err) return nil, fmt.Errorf("invalid client (client is nil)")
} }
output := "# ========== GENERATED BY OCHAMI CONFIGURATOR ==========\n"
output += "# ======================================================" // if we have a client, try making the request for the ethernet interfaces
data := exec.NewContext(map[string]any{ eths, err := client.FetchEthernetInterfaces(opts...)
"hosts": output, if err != nil {
}) return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err)
b := bytes.Buffer{}
if err = t.Execute(&b, data); err != nil {
return nil, fmt.Errorf("failed to execute: %v", err)
} }
return nil, nil
// 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("template: \n%s\n ethernet interfaces found: %v\n", strings.Join(target.Templates, "\n\t"), len(eths))
}
}
// fetch redfish endpoints and handle errors
eps, err := client.FetchRedfishEndpoints(opts...)
if err != nil {
return nil, fmt.Errorf("failed to fetch redfish endpoints: %v", err)
}
if len(eps) <= 0 {
return nil, fmt.Errorf("no redfish endpoints found")
}
// format output for template substitution
nodeEntries := ""
// load files and templates and copy to outputs
files, err := generator.LoadFiles(target.FilePaths...)
if err != nil {
return nil, fmt.Errorf("failed to load files: %v", err)
}
templates, err := generator.ApplyTemplates(generator.Mappings{
"node_entries": nodeEntries,
}, target.Templates...)
if err != nil {
return nil, fmt.Errorf("failed to load templates: %v", err)
}
maps.Copy(outputs, 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)
}
}
}
return outputs, err
} }
var Generator Warewulf var Generator Warewulf

View file

@ -4,11 +4,13 @@
package server package server
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
configurator "github.com/OpenCHAMI/configurator/internal" configurator "github.com/OpenCHAMI/configurator/internal"
"github.com/OpenCHAMI/configurator/internal/generator"
"github.com/OpenCHAMI/jwtauth/v5" "github.com/OpenCHAMI/jwtauth/v5"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
@ -19,9 +21,16 @@ var (
tokenAuth *jwtauth.JWTAuth = nil tokenAuth *jwtauth.JWTAuth = nil
) )
type Jwks struct {
Uri string
Retries int
}
type Server struct { type Server struct {
*http.Server *http.Server
JwksUri string `yaml:"jwks-uri"` Config *configurator.Config
Jwks Jwks `yaml:"jwks"`
GeneratorParams generator.Params
TokenAuth *jwtauth.JWTAuth
} }
func New() *Server { func New() *Server {
@ -29,25 +38,28 @@ func New() *Server {
Server: &http.Server{ Server: &http.Server{
Addr: "localhost:3334", Addr: "localhost:3334",
}, },
JwksUri: "", Jwks: Jwks{
Uri: "",
Retries: 5,
},
} }
} }
func (s *Server) Start(config *configurator.Config) error { func (s *Server) Serve() error {
// create client just for the server to use to fetch data from SMD // create client just for the server to use to fetch data from SMD
_ = &configurator.SmdClient{ _ = &configurator.SmdClient{
Host: config.SmdClient.Host, Host: s.Config.SmdClient.Host,
Port: config.SmdClient.Port, Port: s.Config.SmdClient.Port,
} }
// set the server address with config values // set the server address with config values
s.Server.Addr = fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port) s.Server.Addr = fmt.Sprintf("%s:%d", s.Config.Server.Host, s.Config.Server.Port)
// fetch JWKS public key from authorization server // fetch JWKS public key from authorization server
if config.Server.Jwks.Uri != "" && tokenAuth == nil { if s.Config.Server.Jwks.Uri != "" && tokenAuth == nil {
for i := 0; i < config.Server.Jwks.Retries; i++ { for i := 0; i < s.Config.Server.Jwks.Retries; i++ {
var err error var err error
tokenAuth, err = configurator.FetchPublicKeyFromURL(config.Server.Jwks.Uri) tokenAuth, err = configurator.FetchPublicKeyFromURL(s.Config.Server.Jwks.Uri)
if err != nil { if err != nil {
logrus.Errorf("failed to fetch JWKS: %w", err) logrus.Errorf("failed to fetch JWKS: %w", err)
continue continue
@ -58,52 +70,73 @@ func (s *Server) Start(config *configurator.Config) error {
// create new go-chi router with its routes // create new go-chi router with its routes
router := chi.NewRouter() router := chi.NewRouter()
router.Use(middleware.RedirectSlashes) router.Use(middleware.RequestID)
router.Use(middleware.RealIP)
router.Use(middleware.Logger)
router.Use(middleware.Recoverer)
router.Use(middleware.StripSlashes)
router.Use(middleware.Timeout(60 * time.Second)) router.Use(middleware.Timeout(60 * time.Second))
router.Group(func(r chi.Router) { if s.Config.Server.Jwks.Uri != "" {
if config.Server.Jwks.Uri != "" { router.Group(func(r chi.Router) {
r.Use( r.Use(
jwtauth.Verifier(tokenAuth), jwtauth.Verifier(tokenAuth),
jwtauth.Authenticator(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"),
// }
// NOTE: we probably don't want to hardcode the types, but should do for now // protected routes if using auth
// if _type == "dhcp" { r.HandleFunc("/generate", s.Generate)
// // fetch eths from SMD r.HandleFunc("/templates", s.ManageTemplates)
// 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)
// }
}) })
r.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) { } else {
// TODO: handle GET request // public routes without auth
// TODO: handle POST request router.HandleFunc("/generate", s.Generate)
router.HandleFunc("/templates", s.ManageTemplates)
}
// always public routes go here (none at the moment)
})
})
s.Handler = router s.Handler = router
return s.ListenAndServe() return s.ListenAndServe()
} }
func WriteError(w http.ResponseWriter, format string, a ...any) {
errmsg := fmt.Sprintf(format, a...)
fmt.Printf(errmsg)
w.Write([]byte(errmsg))
}
func (s *Server) Generate(w http.ResponseWriter, r *http.Request) {
s.GeneratorParams.Target = r.URL.Query().Get("target")
outputs, err := generator.Generate(s.Config, s.GeneratorParams)
if err != nil {
WriteError(w, "failed to generate config: %v", err)
return
}
// convert byte arrays to string
tmp := map[string]string{}
for path, output := range outputs {
tmp[path] = string(output)
}
// marshal output to JSON then send
b, err := json.Marshal(tmp)
if err != nil {
WriteError(w, "failed to marshal output: %v", err)
return
}
_, err = w.Write(b)
if err != nil {
WriteError(w, "failed to write response: %v", err)
return
}
}
func (s *Server) ManageTemplates(w http.ResponseWriter, r *http.Request) {
// TODO: need to implement template managing API first in "internal/generator/templates" or something
_, err := w.Write([]byte("this is not implemented yet"))
if err != nil {
WriteError(w, "failed to write response: %v", err)
return
}
}

View file

@ -35,3 +35,20 @@ func AssertOptionsExist(params Params, opts ...string) []string {
} }
return foundKeys 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
}

View file

@ -7,6 +7,8 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"os/exec"
"strings"
) )
func PathExists(path string) (bool, error) { func PathExists(path string) (bool, error) {
@ -26,7 +28,7 @@ func MakeRequest(url string, httpMethod string, body []byte, headers map[string]
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("could not create new HTTP request: %v", err) return nil, nil, fmt.Errorf("could not create new HTTP request: %v", err)
} }
req.Header.Add("User-Agent", "magellan") req.Header.Add("User-Agent", "configurator")
for k, v := range headers { for k, v := range headers {
req.Header.Add(k, v) req.Header.Add(k, v)
} }
@ -41,3 +43,38 @@ func MakeRequest(url string, httpMethod string, body []byte, headers map[string]
} }
return res, b, err return res, b, err
} }
func ConvertMapOutput(m map[string][]byte) map[string]string {
n := make(map[string]string, len(m))
for k, v := range m {
n[k] = string(v)
}
return n
}
func GitCommit() string {
c := exec.Command("git", "rev-parse", "HEAD")
stdout, err := c.Output()
if err != nil {
return ""
}
return strings.TrimRight(string(stdout), "\n")
}
// NOTE: would it be better to use slices.DeleteFunc instead
func RemoveIndex[T comparable](s []T, index int) []T {
ret := make([]T, 0)
ret = append(ret, s[:index]...)
return append(ret, s[index+1:]...)
}
func CopyIf[T comparable](s []T, condition func(t T) bool) []T {
var f = make([]T, 0)
for _, e := range s {
if condition(e) {
f = append(f, e)
}
}
return f
}