Moved internal/ to pkg/ to make building external plugins possible
This commit is contained in:
parent
075b1a1f7f
commit
7361ec739f
19 changed files with 4 additions and 3 deletions
114
internal/auth.go
114
internal/auth.go
|
|
@ -1,114 +0,0 @@
|
|||
package configurator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/OpenCHAMI/jwtauth/v5"
|
||||
"github.com/lestrrat-go/jwx/jwk"
|
||||
)
|
||||
|
||||
func VerifyClaims(testClaims []string, r *http.Request) (bool, error) {
|
||||
// extract claims from JWT
|
||||
_, claims, err := jwtauth.FromContext(r.Context())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get claims(s) from token: %v", err)
|
||||
}
|
||||
|
||||
// verify that each one of the test claims are included
|
||||
for _, testClaim := range testClaims {
|
||||
_, ok := claims[testClaim]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("failed to verify claim(s) from token: %s", testClaim)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func VerifyScope(testScopes []string, r *http.Request) (bool, error) {
|
||||
// extract the scopes from JWT
|
||||
var scopes []string
|
||||
_, claims, err := jwtauth.FromContext(r.Context())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get claim(s) from token: %v", err)
|
||||
}
|
||||
|
||||
appendScopes := func(slice []string, scopeClaim any) []string {
|
||||
switch scopeClaim.(type) {
|
||||
case []any:
|
||||
// convert all scopes to str and append
|
||||
for _, s := range scopeClaim.([]any) {
|
||||
switch s.(type) {
|
||||
case string:
|
||||
slice = append(slice, s.(string))
|
||||
}
|
||||
}
|
||||
case []string:
|
||||
slice = append(slice, scopeClaim.([]string)...)
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// check for and append both "scp" and "scope" claims
|
||||
v, ok := claims["scp"]
|
||||
if ok {
|
||||
scopes = appendScopes(scopes, v)
|
||||
}
|
||||
v, ok = claims["scope"]
|
||||
if ok {
|
||||
scopes = appendScopes(scopes, v)
|
||||
}
|
||||
|
||||
// check for both 'scp' and 'scope' claims for scope
|
||||
scopeClaim, ok := claims["scp"]
|
||||
if ok {
|
||||
switch scopeClaim.(type) {
|
||||
case []any:
|
||||
// convert all scopes to str and append
|
||||
for _, s := range scopeClaim.([]any) {
|
||||
switch s.(type) {
|
||||
case string:
|
||||
scopes = append(scopes, s.(string))
|
||||
}
|
||||
}
|
||||
case []string:
|
||||
scopes = append(scopes, scopeClaim.([]string)...)
|
||||
}
|
||||
}
|
||||
scopeClaim, ok = claims["scope"]
|
||||
if ok {
|
||||
scopes = append(scopes, scopeClaim.([]string)...)
|
||||
}
|
||||
|
||||
// verify that each of the test scopes are included
|
||||
for _, testScope := range testScopes {
|
||||
index := slices.Index(scopes, testScope)
|
||||
if index < 0 {
|
||||
return false, fmt.Errorf("invalid or missing scope")
|
||||
}
|
||||
}
|
||||
// NOTE: should this be ok if no scopes were found?
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func FetchPublicKeyFromURL(url string) (*jwtauth.JWTAuth, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
set, err := jwk.Fetch(ctx, url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v", err)
|
||||
}
|
||||
jwks, err := json.Marshal(set)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal JWKS: %v", err)
|
||||
}
|
||||
tokenAuth, err := jwtauth.NewKeySet(jwks)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize JWKS: %v", err)
|
||||
}
|
||||
|
||||
return tokenAuth, nil
|
||||
}
|
||||
|
|
@ -1,239 +0,0 @@
|
|||
package configurator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/internal/util"
|
||||
)
|
||||
|
||||
type ClientOption func(*SmdClient)
|
||||
|
||||
// An struct that's meant to extend functionality of the base HTTP client by
|
||||
// adding commonly made requests to SMD. The implemented functions are can be
|
||||
// used in generator plugins to fetch data when it is needed to substitute
|
||||
// values for the Jinja templates used.
|
||||
type SmdClient struct {
|
||||
http.Client `json:"-"`
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
AccessToken string `yaml:"access-token"`
|
||||
}
|
||||
|
||||
// Constructor function that allows supplying ClientOption arguments to set
|
||||
// things like the host, port, access token, etc.
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Need to check for errors when reading from a file
|
||||
func WithCertPoolFile(certPath string) ClientOption {
|
||||
if certPath == "" {
|
||||
return func(sc *SmdClient) {}
|
||||
}
|
||||
cacert, _ := os.ReadFile(certPath)
|
||||
certPool := x509.NewCertPool()
|
||||
certPool.AppendCertsFromPEM(cacert)
|
||||
return WithCertPool(certPool)
|
||||
}
|
||||
|
||||
func WithVerbosity() util.Option {
|
||||
return func(p util.Params) {
|
||||
p["verbose"] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Create a set of params with all default values.
|
||||
func NewParams() util.Params {
|
||||
return util.Params{
|
||||
"verbose": false,
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the ethernet interfaces from SMD service using its API. An access token may be required if the SMD
|
||||
// service SMD_JWKS_URL envirnoment variable is set.
|
||||
func (client *SmdClient) FetchEthernetInterfaces(opts ...util.Option) ([]EthernetInterface, error) {
|
||||
var (
|
||||
params = util.GetParams(opts...)
|
||||
verbose = util.Get[bool](params, "verbose")
|
||||
eths = []EthernetInterface{}
|
||||
)
|
||||
// make request to SMD endpoint
|
||||
b, err := client.makeRequest("/Inventory/EthernetInterfaces")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read HTTP response: %v", err)
|
||||
}
|
||||
|
||||
// unmarshal response body JSON and extract in object
|
||||
err = json.Unmarshal(b, ðs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
// print what we got if verbose is set
|
||||
if verbose != nil {
|
||||
if *verbose {
|
||||
fmt.Printf("Ethernet Interfaces: %v\n", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
return eths, nil
|
||||
}
|
||||
|
||||
// Fetch the components from SMD using its API. An access token may be required if the SMD
|
||||
// service SMD_JWKS_URL envirnoment variable is set.
|
||||
func (client *SmdClient) FetchComponents(opts ...util.Option) ([]Component, error) {
|
||||
var (
|
||||
params = util.GetParams(opts...)
|
||||
verbose = util.Get[bool](params, "verbose")
|
||||
comps = []Component{}
|
||||
)
|
||||
// make request to SMD endpoint
|
||||
b, err := client.makeRequest("/State/Components")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to 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
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
// print what we got if verbose is set
|
||||
if verbose != nil {
|
||||
if *verbose {
|
||||
fmt.Printf("Components: %v\n", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("client is nil")
|
||||
}
|
||||
|
||||
// fetch DHCP related information from SMD's endpoint:
|
||||
url := fmt.Sprintf("%s:%d/hsm/v2%s", client.Host, client.Port, endpoint)
|
||||
req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer([]byte{}))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new HTTP request: %v", err)
|
||||
}
|
||||
|
||||
// include access token in authorzation header if found
|
||||
// NOTE: This shouldn't be needed for this endpoint since it's public
|
||||
if client.AccessToken != "" {
|
||||
req.Header.Add("Authorization", "Bearer "+client.AccessToken)
|
||||
}
|
||||
|
||||
// make the request to SMD
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make request: %v", err)
|
||||
}
|
||||
|
||||
// read the contents of the response body
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
package configurator
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Options struct{}
|
||||
|
||||
type Target struct {
|
||||
Templates []string `yaml:"templates,omitempty"`
|
||||
FilePaths []string `yaml:"files,omitempty"`
|
||||
RunTargets []string `yaml:"targets,omitempty"`
|
||||
}
|
||||
|
||||
type Jwks struct {
|
||||
Uri string `yaml:"uri"`
|
||||
Retries int `yaml:"retries"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Jwks Jwks `yaml:"jwks"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Version string `yaml:"version"`
|
||||
Server Server `yaml:"server"`
|
||||
SmdClient SmdClient `yaml:"smd"`
|
||||
AccessToken string `yaml:"access-token"`
|
||||
Targets map[string]Target `yaml:"targets"`
|
||||
PluginDirs []string `yaml:"plugins"`
|
||||
CertPath string `yaml:"ca-cert"`
|
||||
Options Options `yaml:"options"`
|
||||
}
|
||||
|
||||
// Creates a new config with default parameters.
|
||||
func NewConfig() Config {
|
||||
return Config{
|
||||
Version: "",
|
||||
SmdClient: SmdClient{
|
||||
Host: "http://127.0.0.1",
|
||||
Port: 27779,
|
||||
},
|
||||
Targets: map[string]Target{
|
||||
"dnsmasq": Target{
|
||||
Templates: []string{},
|
||||
},
|
||||
"conman": Target{
|
||||
Templates: []string{},
|
||||
},
|
||||
"warewulf": Target{
|
||||
Templates: []string{
|
||||
"templates/warewulf/defaults/node.jinja",
|
||||
"templates/warewulf/defaults/provision.jinja",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
PluginDirs: []string{},
|
||||
Server: Server{
|
||||
Host: "127.0.0.1",
|
||||
Port: 3334,
|
||||
Jwks: Jwks{
|
||||
Uri: "",
|
||||
Retries: 5,
|
||||
},
|
||||
},
|
||||
Options: Options{},
|
||||
}
|
||||
}
|
||||
|
||||
func LoadConfig(path string) Config {
|
||||
var c Config = NewConfig()
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Printf("failed to read config file: %v\n", err)
|
||||
return c
|
||||
}
|
||||
err = yaml.Unmarshal(file, &c)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to unmarshal config: %v\n", err)
|
||||
return c
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (config *Config) SaveConfig(path string) {
|
||||
path = filepath.Clean(path)
|
||||
if path == "" || path == "." {
|
||||
path = "config.yaml"
|
||||
}
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
log.Printf("failed to marshal config: %v\n", err)
|
||||
return
|
||||
}
|
||||
err = os.WriteFile(path, data, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Printf("failed to write default config file: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func SaveDefaultConfig(path string) {
|
||||
path = filepath.Clean(path)
|
||||
if path == "" || path == "." {
|
||||
path = "config.yaml"
|
||||
}
|
||||
var c = NewConfig()
|
||||
data, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
log.Printf("failed to marshal config: %v\n", err)
|
||||
return
|
||||
}
|
||||
err = os.WriteFile(path, data, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Printf("failed to write default config file: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
package configurator
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type IPAddr struct {
|
||||
IpAddress string `json:"IPAddress"`
|
||||
Network string `json:"Network"`
|
||||
}
|
||||
|
||||
type EthernetInterface struct {
|
||||
Id string
|
||||
Description string
|
||||
MacAddress string
|
||||
LastUpdate string
|
||||
ComponentId string
|
||||
Type string
|
||||
IpAddresses []IPAddr
|
||||
}
|
||||
|
||||
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 BMC struct {
|
||||
}
|
||||
|
|
@ -1,320 +0,0 @@
|
|||
package generator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/internal"
|
||||
"github.com/OpenCHAMI/configurator/internal/util"
|
||||
"github.com/nikolalohinski/gonja/v2"
|
||||
"github.com/nikolalohinski/gonja/v2/exec"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Mappings map[string]any
|
||||
type FileMap map[string][]byte
|
||||
type FileList [][]byte
|
||||
|
||||
// Generator interface used to define how files are created. Plugins can
|
||||
// be created entirely independent of the main driver program.
|
||||
type Generator interface {
|
||||
GetName() string
|
||||
GetVersion() string
|
||||
GetDescription() string
|
||||
Generate(config *configurator.Config, opts ...util.Option) (FileMap, error)
|
||||
}
|
||||
|
||||
// Params defined and used by the "generate" subcommand.
|
||||
type Params struct {
|
||||
Args []string
|
||||
PluginPaths []string
|
||||
Target string
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
// Converts the file outputs from map[string][]byte to map[string]string.
|
||||
func ConvertContentsToString(f FileMap) map[string]string {
|
||||
n := make(map[string]string, len(f))
|
||||
for k, v := range f {
|
||||
n[k] = string(v)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Loads files without applying any Jinja 2 templating.
|
||||
func LoadFiles(paths ...string) (FileMap, error) {
|
||||
var outputs = FileMap{}
|
||||
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 outputs, nil
|
||||
}
|
||||
|
||||
// Loads a single generator plugin given a single file path.
|
||||
func LoadPlugin(path string) (Generator, error) {
|
||||
// skip loading plugin 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 path is directory: %v", err)
|
||||
}
|
||||
|
||||
// try and open the plugin
|
||||
p, err := plugin.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open plugin: %v", err)
|
||||
}
|
||||
|
||||
// load the "Generator" symbol from plugin
|
||||
symbol, err := p.Lookup("Generator")
|
||||
if err != nil {
|
||||
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)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to load the correct symbol type at path '%s'", path)
|
||||
}
|
||||
return gen, nil
|
||||
}
|
||||
|
||||
// Loads all generator plugins in a given directory.
|
||||
//
|
||||
// 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) {
|
||||
// check if verbose option is supplied
|
||||
var (
|
||||
gens = make(map[string]Generator)
|
||||
params = util.GetParams(opts...)
|
||||
)
|
||||
|
||||
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 gens, 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 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: %v", err)
|
||||
}
|
||||
|
||||
// execute/render jinja template
|
||||
b := bytes.Buffer{}
|
||||
if err = t.Execute(&b, data); err != nil {
|
||||
return nil, fmt.Errorf("failed to execute: %v", 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: %v", err)
|
||||
}
|
||||
|
||||
// execute/render jinja template
|
||||
b := bytes.Buffer{}
|
||||
if err = t.Execute(&b, data); err != nil {
|
||||
return nil, fmt.Errorf("failed to execute: %v", err)
|
||||
}
|
||||
outputs[path] = b.Bytes()
|
||||
}
|
||||
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
// Main function to generate a collection of files as a map with the path as the key and
|
||||
// the contents of the file as the value. This function currently expects a list of plugin
|
||||
// paths to load all plugins within a directory. Then, each plugin's generator.Generate()
|
||||
// function is called for each target specified.
|
||||
//
|
||||
// This function is the corresponding implementation for the "generate" CLI subcommand.
|
||||
// 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 Generate(config *configurator.Config, params Params) (FileMap, 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.WithCertPoolFile(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")
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/internal"
|
||||
"github.com/OpenCHAMI/configurator/internal/generator"
|
||||
"github.com/OpenCHAMI/configurator/internal/util"
|
||||
)
|
||||
|
||||
type Conman struct{}
|
||||
|
||||
func (g *Conman) GetName() string {
|
||||
return "conman"
|
||||
}
|
||||
|
||||
func (g *Conman) GetVersion() string {
|
||||
return util.GitCommit()
|
||||
}
|
||||
|
||||
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) (generator.FileMap, error) {
|
||||
var (
|
||||
params = generator.GetParams(opts...)
|
||||
client = generator.GetClient(params)
|
||||
targetKey = params["targets"].(string) // required param
|
||||
target = config.Targets[targetKey]
|
||||
eps []configurator.RedfishEndpoint = nil
|
||||
err error = nil
|
||||
// serverOpts = ""
|
||||
// globalOpts = ""
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.ApplyTemplateFromFiles(generator.Mappings{
|
||||
"plugin_name": g.GetName(),
|
||||
"plugin_version": g.GetVersion(),
|
||||
"plugin_description": g.GetDescription(),
|
||||
"server_opts": "",
|
||||
"global_opts": "",
|
||||
}, target.Templates...)
|
||||
}
|
||||
|
||||
var Generator Conman
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/internal"
|
||||
"github.com/OpenCHAMI/configurator/internal/generator"
|
||||
"github.com/OpenCHAMI/configurator/internal/util"
|
||||
)
|
||||
|
||||
type CoreDhcp struct{}
|
||||
|
||||
func (g *CoreDhcp) GetName() string {
|
||||
return "coredhcp"
|
||||
}
|
||||
|
||||
func (g *CoreDhcp) GetVersion() string {
|
||||
return util.GitCommit()
|
||||
}
|
||||
|
||||
func (g *CoreDhcp) GetDescription() string {
|
||||
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) (generator.FileMap, error) {
|
||||
return nil, fmt.Errorf("plugin does not implement generation function")
|
||||
}
|
||||
|
||||
var Generator CoreDhcp
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
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.FileMap, 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.ApplyTemplateFromFiles(generator.Mappings{
|
||||
"plugin_name": g.GetName(),
|
||||
"plugin_version": g.GetVersion(),
|
||||
"plugin_description": g.GetDescription(),
|
||||
"compute_nodes": compute_nodes,
|
||||
"node_entries": "",
|
||||
}, target.Templates...)
|
||||
}
|
||||
|
||||
var Generator Dhcpd
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/internal"
|
||||
"github.com/OpenCHAMI/configurator/internal/generator"
|
||||
"github.com/OpenCHAMI/configurator/internal/util"
|
||||
)
|
||||
|
||||
type DnsMasq struct{}
|
||||
|
||||
func (g *DnsMasq) GetName() string {
|
||||
return "dnsmasq"
|
||||
}
|
||||
|
||||
func (g *DnsMasq) GetVersion() string {
|
||||
return util.GitCommit()
|
||||
}
|
||||
|
||||
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) (generator.FileMap, error) {
|
||||
// make sure we have a valid config first
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("invalid config (config is nil)")
|
||||
}
|
||||
|
||||
// set all the defaults for variables
|
||||
var (
|
||||
params = generator.GetParams(opts...)
|
||||
client = generator.GetClient(params)
|
||||
targetKey = params["target"].(string) // required param
|
||||
target = config.Targets[targetKey]
|
||||
eths []configurator.EthernetInterface = nil
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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\nethernet interfaces found: %v\n", strings.Join(target.Templates, "\n\t"), len(eths))
|
||||
}
|
||||
}
|
||||
|
||||
// format output to write to config file
|
||||
output := "# ========== DYNAMICALLY GENERATED BY OPENCHAMI CONFIGURATOR ==========\n"
|
||||
for _, eth := range eths {
|
||||
if eth.Type == "NodeBMC" {
|
||||
output += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n"
|
||||
} else {
|
||||
output += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n"
|
||||
}
|
||||
}
|
||||
output += "# ====================================================================="
|
||||
|
||||
// apply template substitutions and return output as byte array
|
||||
return generator.ApplyTemplateFromFiles(generator.Mappings{
|
||||
"plugin_name": g.GetName(),
|
||||
"plugin_version": g.GetVersion(),
|
||||
"plugin_description": g.GetDescription(),
|
||||
"dhcp-hosts": output,
|
||||
}, target.Templates...)
|
||||
}
|
||||
|
||||
var Generator DnsMasq
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
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.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
|
||||
}
|
||||
|
||||
var Generator Example
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/internal"
|
||||
"github.com/OpenCHAMI/configurator/internal/generator"
|
||||
"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) (generator.FileMap, error) {
|
||||
return nil, fmt.Errorf("plugin does not implement generation function")
|
||||
}
|
||||
|
||||
var Generator Hostfile
|
||||
|
|
@ -1 +0,0 @@
|
|||
package main
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/internal"
|
||||
"github.com/OpenCHAMI/configurator/internal/generator"
|
||||
"github.com/OpenCHAMI/configurator/internal/util"
|
||||
)
|
||||
|
||||
type Powerman struct{}
|
||||
|
||||
func (g *Powerman) GetName() string {
|
||||
return "powerman"
|
||||
}
|
||||
|
||||
func (g *Powerman) GetVersion() string {
|
||||
return util.GitCommit()
|
||||
}
|
||||
|
||||
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) (generator.FileMap, error) {
|
||||
return nil, fmt.Errorf("plugin does not implement generation function")
|
||||
}
|
||||
|
||||
var Generator Powerman
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/internal"
|
||||
"github.com/OpenCHAMI/configurator/internal/generator"
|
||||
"github.com/OpenCHAMI/configurator/internal/util"
|
||||
)
|
||||
|
||||
type Syslog struct{}
|
||||
|
||||
func (g *Syslog) GetName() string {
|
||||
return "syslog"
|
||||
}
|
||||
|
||||
func (g *Syslog) GetVersion() string {
|
||||
return util.GitCommit()
|
||||
}
|
||||
|
||||
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) (generator.FileMap, error) {
|
||||
return nil, fmt.Errorf("plugin does not implement generation function")
|
||||
}
|
||||
|
||||
var Generator Syslog
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/internal"
|
||||
"github.com/OpenCHAMI/configurator/internal/generator"
|
||||
"github.com/OpenCHAMI/configurator/internal/util"
|
||||
)
|
||||
|
||||
type Warewulf struct{}
|
||||
|
||||
func (g *Warewulf) GetName() string {
|
||||
return "warewulf"
|
||||
}
|
||||
|
||||
func (g *Warewulf) GetVersion() string {
|
||||
return util.GitCommit()
|
||||
}
|
||||
|
||||
func (g *Warewulf) GetDescription() string {
|
||||
return "Configurator generator plugin for 'warewulf' config files."
|
||||
}
|
||||
|
||||
func (g *Warewulf) Generate(config *configurator.Config, opts ...util.Option) (generator.FileMap, error) {
|
||||
var (
|
||||
params = generator.GetParams(opts...)
|
||||
client = generator.GetClient(params)
|
||||
targetKey = params["target"].(string)
|
||||
target = config.Targets[targetKey]
|
||||
outputs = make(generator.FileMap, len(target.FilePaths)+len(target.Templates))
|
||||
)
|
||||
|
||||
// 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...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch ethernet interfaces with client: %v", err)
|
||||
}
|
||||
|
||||
// check if we have the required params first
|
||||
if eths == nil {
|
||||
return nil, fmt.Errorf("invalid ethernet interfaces (variable is nil)")
|
||||
}
|
||||
if len(eths) <= 0 {
|
||||
return nil, fmt.Errorf("no ethernet interfaces found")
|
||||
}
|
||||
|
||||
// print message if verbose param found
|
||||
if verbose, ok := params["verbose"].(bool); ok {
|
||||
if verbose {
|
||||
fmt.Printf("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.ApplyTemplateFromFiles(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
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
// TODO: implement a way to fetch schemas from node orchestrator
|
||||
package configurator
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
//go:build server || all
|
||||
// +build server all
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/internal"
|
||||
"github.com/OpenCHAMI/configurator/internal/generator"
|
||||
"github.com/OpenCHAMI/jwtauth/v5"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenAuth *jwtauth.JWTAuth = nil
|
||||
)
|
||||
|
||||
type Jwks struct {
|
||||
Uri string
|
||||
Retries int
|
||||
}
|
||||
type Server struct {
|
||||
*http.Server
|
||||
Config *configurator.Config
|
||||
Jwks Jwks `yaml:"jwks"`
|
||||
GeneratorParams generator.Params
|
||||
TokenAuth *jwtauth.JWTAuth
|
||||
}
|
||||
|
||||
// Constructor to make a new server instance with an optional config.
|
||||
func New(config *configurator.Config) *Server {
|
||||
// create default config if none supplied
|
||||
if config == nil {
|
||||
c := configurator.NewConfig()
|
||||
config = &c
|
||||
}
|
||||
// return based on config values
|
||||
return &Server{
|
||||
Config: config,
|
||||
Server: &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port),
|
||||
},
|
||||
Jwks: Jwks{
|
||||
Uri: config.Server.Jwks.Uri,
|
||||
Retries: config.Server.Jwks.Retries,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Main function to start up configurator as a service.
|
||||
func (s *Server) Serve() error {
|
||||
// create client just for the server to use to fetch data from SMD
|
||||
_ = &configurator.SmdClient{
|
||||
Host: s.Config.SmdClient.Host,
|
||||
Port: s.Config.SmdClient.Port,
|
||||
}
|
||||
|
||||
// set the server address with config values
|
||||
s.Server.Addr = fmt.Sprintf("%s:%d", s.Config.Server.Host, s.Config.Server.Port)
|
||||
|
||||
// fetch JWKS public key from authorization server
|
||||
if s.Config.Server.Jwks.Uri != "" && tokenAuth == nil {
|
||||
for i := 0; i < s.Config.Server.Jwks.Retries; i++ {
|
||||
var err error
|
||||
tokenAuth, err = configurator.FetchPublicKeyFromURL(s.Config.Server.Jwks.Uri)
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to fetch JWKS: %w", err)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// create new go-chi router with its routes
|
||||
router := chi.NewRouter()
|
||||
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))
|
||||
if s.Config.Server.Jwks.Uri != "" {
|
||||
router.Group(func(r chi.Router) {
|
||||
r.Use(
|
||||
jwtauth.Verifier(tokenAuth),
|
||||
jwtauth.Authenticator(tokenAuth),
|
||||
)
|
||||
|
||||
// protected routes if using auth
|
||||
r.HandleFunc("/generate", s.Generate)
|
||||
r.HandleFunc("/templates", s.ManageTemplates)
|
||||
})
|
||||
} else {
|
||||
// public routes without auth
|
||||
router.HandleFunc("/generate", s.Generate)
|
||||
router.HandleFunc("/templates", s.ManageTemplates)
|
||||
}
|
||||
|
||||
// always available public routes go here (none at the moment)
|
||||
|
||||
s.Handler = router
|
||||
return s.ListenAndServe()
|
||||
}
|
||||
|
||||
// This is the corresponding service function to generate templated files, that
|
||||
// works similarly to the CLI variant. This function takes similiar arguments as
|
||||
// query parameters that are included in the HTTP request URL.
|
||||
func (s *Server) Generate(w http.ResponseWriter, r *http.Request) {
|
||||
// get all of the expect query URL params and validate
|
||||
s.GeneratorParams.Target = r.URL.Query().Get("target")
|
||||
if s.GeneratorParams.Target == "" {
|
||||
writeError(w, "no targets supplied")
|
||||
return
|
||||
}
|
||||
|
||||
// generate a new config file from supplied params
|
||||
outputs, err := generator.Generate(s.Config, s.GeneratorParams)
|
||||
if err != nil {
|
||||
writeError(w, "failed to generate config: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// marshal output to JSON then send response to client
|
||||
tmp := generator.ConvertContentsToString(outputs)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Incomplete WIP function for managing templates remotely. There is currently no
|
||||
// internal API to do this yet.
|
||||
//
|
||||
// TODO: need to implement template managing API first in "internal/generator/templates" or something
|
||||
func (s *Server) ManageTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := w.Write([]byte("this is not implemented yet"))
|
||||
if err != nil {
|
||||
writeError(w, "failed to write response: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper function to simplify writting error message responses. This function
|
||||
// is only intended to be used with the service and nothing else.
|
||||
func writeError(w http.ResponseWriter, format string, a ...any) {
|
||||
errmsg := fmt.Sprintf(format, a...)
|
||||
fmt.Printf(errmsg)
|
||||
w.Write([]byte(errmsg))
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
// Params are accessible in generator.Generate().
|
||||
type Params map[string]any
|
||||
type Option func(Params)
|
||||
|
||||
// Extract all parameters from the options passed as map[string]any.
|
||||
func GetParams(opts ...Option) Params {
|
||||
params := Params{}
|
||||
for _, opt := range opts {
|
||||
opt(params)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// Test if an option is present in params
|
||||
func (p *Params) OptionExists(params Params, opt string) bool {
|
||||
var k []string = maps.Keys(params)
|
||||
return slices.Contains(k, opt)
|
||||
}
|
||||
|
||||
// Assert that the options exists within the params map
|
||||
func AssertOptionsExist(params Params, opts ...string) []string {
|
||||
foundKeys := []string{}
|
||||
for k := range params {
|
||||
index := slices.IndexFunc(opts, func(s string) bool {
|
||||
return s == k
|
||||
})
|
||||
if index >= 0 {
|
||||
foundKeys = append(foundKeys, k)
|
||||
}
|
||||
}
|
||||
return foundKeys
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Wrapper function to simplify checking if a path exists.
|
||||
func PathExists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Wrapper function to simplify checking if a path is a directory.
|
||||
func IsDirectory(path string) (bool, error) {
|
||||
// This returns an *os.FileInfo type
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to stat path: %v", err)
|
||||
}
|
||||
|
||||
// IsDir is short for fileInfo.Mode().IsDir()
|
||||
return fileInfo.IsDir(), nil
|
||||
}
|
||||
|
||||
// Wrapper function to confine making a HTTP request into a single function
|
||||
// instead of multiple.
|
||||
func MakeRequest(url string, httpMethod string, body []byte, headers map[string]string) (*http.Response, []byte, error) {
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
req, err := http.NewRequest(httpMethod, url, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not create new HTTP request: %v", err)
|
||||
}
|
||||
req.Header.Add("User-Agent", "configurator")
|
||||
for k, v := range headers {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not make request: %v", err)
|
||||
}
|
||||
b, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not read response body: %v", err)
|
||||
}
|
||||
return res, b, err
|
||||
}
|
||||
|
||||
// Returns the git commit string by executing command.
|
||||
// NOTE: This currently requires git to be installed.
|
||||
// TODO: Change how this is done to not require executing a command.
|
||||
func GitCommit() string {
|
||||
c := exec.Command("git", "rev-parse", "HEAD")
|
||||
stdout, err := c.Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimRight(string(stdout), "\n")
|
||||
}
|
||||
|
||||
// General function to remove element by a given index.
|
||||
// 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:]...)
|
||||
}
|
||||
|
||||
// General function to copy elements from slice if condition is true.
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue