From be40387f4a770ff5c51e38492b7de67ee02d311e Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Thu, 7 Mar 2024 17:00:39 -0700 Subject: [PATCH] Added project files --- cmd/config.go | 30 ++++++++++++ cmd/generate.go | 85 +++++++++++++++++++++++++++++++++ cmd/root.go | 54 +++++++++++++++++++++ internal/client.go | 54 +++++++++++++++++++++ internal/config.go | 82 +++++++++++++++++++++++++++++++ internal/config.yaml | 56 ++++++++++++++++++++++ internal/configurator.go | 22 +++++++++ internal/generator/generator.go | 53 ++++++++++++++++++++ internal/util/util.go | 43 +++++++++++++++++ main.go | 7 +++ 10 files changed, 486 insertions(+) create mode 100644 cmd/config.go create mode 100644 cmd/generate.go create mode 100644 cmd/root.go create mode 100644 internal/client.go create mode 100644 internal/config.go create mode 100644 internal/config.yaml create mode 100644 internal/configurator.go create mode 100644 internal/generator/generator.go create mode 100644 internal/util/util.go create mode 100644 main.go diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..6596988 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + configurator "github.com/OpenCHAMI/configurator/internal" + "github.com/OpenCHAMI/configurator/internal/util" +) + +var configCmd = &cobra.Command{ + Use: "config", + Short: "Create a new default config file", + Run: func(cmd *cobra.Command, args []string) { + // create a new config at all args (paths) + for _, path := range args { + // check and make sure something doesn't exist first + if exists, err := util.PathExists(path); exists || err != nil { + fmt.Printf("file or directory exists\n") + continue + } + configurator.SaveDefaultConfig(path) + } + }, +} + +func init() { + rootCmd.AddCommand(configCmd) +} diff --git a/cmd/generate.go b/cmd/generate.go new file mode 100644 index 0000000..bdaa3ba --- /dev/null +++ b/cmd/generate.go @@ -0,0 +1,85 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + configurator "github.com/OpenCHAMI/configurator/internal" + "github.com/OpenCHAMI/configurator/internal/generator" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + targets []string + tokenFetchRetries int +) +var generateCmd = &cobra.Command{ + Use: "generate", + Short: "Create a config file from current system state", + Run: func(cmd *cobra.Command, args []string) { + client := configurator.SmdClient{ + Host: config.SmdHost, + Port: config.SmdPort, + AccessToken: config.AccessToken, + } + + // make sure that we have a token present before trying to make request + if config.AccessToken == "" { + // check if OCHAMI_ACCESS_TOKEN env var is set if no access token is provided and use that instead + + accessToken := os.Getenv("OCHAMI_ACCESS_TOKEN") + if accessToken != "" { + config.AccessToken = accessToken + } else { + fmt.Printf("No token found. Attempting to generate config without one...\n") + } + } + + if targets == nil { + logrus.Errorf("no target supplied (--target type:template)") + } else { + for _, target := range targets { + // split the target and type + tmp := strings.Split(target, ":") + configType := tmp[0] + configTemplate := tmp[1] + + // NOTE: we probably don't want to hardcode the types, but should do for now + if configType == "dhcp" { + // fetch eths from SMD + eths, err := client.FetchEthernetInterfaces() + if err != nil { + logrus.Errorf("failed to fetch DHCP metadata: %v\n", err) + } + if len(eths) <= 0 { + break + } + // generate a new config from that data + g := generator.New() + g.GenerateDHCP(config, configTemplate, eths) + } else if configType == "dns" { + // TODO: fetch from SMD + // TODO: generate config from pulled info + + } else if configType == "syslog" { + + } else if configType == "ansible" { + + } else if configType == "warewulf" { + + } + + } + } + + }, +} + +func init() { + generateCmd.Flags().StringSliceVar(&targets, "target", nil, "set the target configs to make") + generateCmd.Flags().IntVar(&tokenFetchRetries, "fetch-retries", 5, "set the number of retries to fetch an access token") + + rootCmd.AddCommand(generateCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..a06a0f9 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "fmt" + "os" + + configurator "github.com/OpenCHAMI/configurator/internal" + "github.com/OpenCHAMI/configurator/internal/util" + "github.com/spf13/cobra" +) + +var ( + configPath string + config *configurator.Config +) + +var rootCmd = &cobra.Command{ + Use: "configurator", + Short: "Tool for building common config files", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.Help() + os.Exit(0) + } + }, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + rootCmd.PersistentFlags().StringVar(&configPath, "config", "", "set the config path") +} + +func initConfig() { + if configPath != "" { + exists, err := util.PathExists(configPath) + if err != nil { + fmt.Printf("failed to load config") + os.Exit(1) + } else if exists { + config = configurator.LoadConfig(configPath) + } else { + config = configurator.NewConfig() + } + } else { + config = configurator.NewConfig() + } +} diff --git a/internal/client.go b/internal/client.go new file mode 100644 index 0000000..75d1776 --- /dev/null +++ b/internal/client.go @@ -0,0 +1,54 @@ +package configurator + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +type SmdClient struct { + http.Client + Host string + Port int + AccessToken string +} + +func (client *SmdClient) FetchDNS(config *Config) error { + // fetch DNS related information from SMD's endpoint: + return nil +} + +func (client *SmdClient) FetchEthernetInterfaces() ([]EthernetInterface, error) { + if client == nil { + return nil, fmt.Errorf("client is nil") + } + // fetch DHCP related information from SMD's endpoint: + url := fmt.Sprintf("%s:%d/hsm/v2/Inventory/EthernetInterfaces", client.Host, client.Port) + req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer([]byte{})) + + // include access token in authorzation header if found + if client.AccessToken != "" { + req.Header.Add("Authorization", "Bearer "+client.AccessToken) + } + if err != nil { + return nil, fmt.Errorf("failed to create new HTTP request: %v", err) + } + res, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + + b, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read HTTP response: %v", err) + } + + // unmarshal JSON and extract + eths := []EthernetInterface{} // []map[string]any{} + json.Unmarshal(b, ðs) + fmt.Printf("ethernet interfaces: %v\n", string(b)) + + return eths, nil +} diff --git a/internal/config.go b/internal/config.go new file mode 100644 index 0000000..d817677 --- /dev/null +++ b/internal/config.go @@ -0,0 +1,82 @@ +package configurator + +import ( + "log" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +type Config struct { + Version string `yaml:"version"` + SmdHost string `yaml:"smd-host"` + SmdPort int `yaml:"smd-port"` + AccessToken string `yaml:"access-token"` + TemplatePaths map[string]string `yaml:"templates"` +} + +func NewConfig() *Config { + return &Config{ + Version: "", + SmdHost: "http://127.0.0.1", + SmdPort: 27779, + TemplatePaths: map[string]string{ + "dnsmasq": "templates/dhcp/dnsmasq.conf", + "syslog": "templates/syslog/", + "ansible": "templates/ansible", + "powerman": "templates/powerman", + "conman": "templates/conman", + }, + } +} + +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 + } +} diff --git a/internal/config.yaml b/internal/config.yaml new file mode 100644 index 0000000..48308d3 --- /dev/null +++ b/internal/config.yaml @@ -0,0 +1,56 @@ +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 diff --git a/internal/configurator.go b/internal/configurator.go new file mode 100644 index 0000000..d44388a --- /dev/null +++ b/internal/configurator.go @@ -0,0 +1,22 @@ +package configurator + +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 DHCP struct { + Hostname string + MacAddress string + IpAddress []IPAddr +} diff --git a/internal/generator/generator.go b/internal/generator/generator.go new file mode 100644 index 0000000..6fbb849 --- /dev/null +++ b/internal/generator/generator.go @@ -0,0 +1,53 @@ +package generator + +import ( + "os" + + "fmt" + + configurator "github.com/OpenCHAMI/configurator/internal" + "github.com/nikolalohinski/gonja/v2" + "github.com/nikolalohinski/gonja/v2/exec" +) + +type Generator struct { +} + +func New() *Generator { + return &Generator{} +} + +func (g *Generator) GenerateDNS(config *configurator.Config) { + // generate file using jinja template + // TODO: load template file for DNS + // TODO: substitute DNS data fetched from SMD + // TODO: print generated config file to STDOUT +} + +func (g *Generator) GenerateDHCP(config *configurator.Config, target string, eths []configurator.EthernetInterface) error { + // generate file using gonja template + // TODO: load template file for DHCP + path := config.TemplatePaths[target] + fmt.Printf("path: %s\neth count: %v\n", path, len(eths)) + t, err := gonja.FromFile(config.TemplatePaths[target]) + if err != nil { + return fmt.Errorf("failed to read template from file: %v", err) + } + template := "# ========== GENERATED BY OCHAMI CONFIGURATOR ==========\n" + for _, eth := range eths { + if eth.Type == "NodeBMC" { + template += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n" + } else { + template += "dhcp-host=" + eth.MacAddress + "," + eth.ComponentId + "," + eth.IpAddresses[0].IpAddress + "\n" + } + } + template += "# ======================================================" + data := exec.NewContext(map[string]any{ + "hosts": template, + }) + if err = t.Execute(os.Stdout, data); err != nil { + return fmt.Errorf("failed to execute: %v", err) + } + + return nil +} diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..cc37bec --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,43 @@ +package util + +import ( + "bytes" + "crypto/tls" + "fmt" + "io" + "net/http" + "os" +) + +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 +} + +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", "magellan") + 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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..a3e5a86 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/OpenCHAMI/configurator/cmd" + +func main() { + cmd.Execute() +}