diff --git a/CHANGELOG.md b/CHANGELOG.md index 86629650..dac937e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `wwctl --emptyconf genconfig warewulfconf print` for the available paths. - Added experimental dnsmasq support. +- new subcommand `wwctl genconf` is available with following subcommands: + * `completions` which will create the files used for bash-completion. Also + fish an zsh completions can be generated + * `defaults` which will generate a valid `defaults.conf` + * `man` which will generate the man pages in the specified directory + * `reference` which will generate a reference documentation for the wwctl commands + * `warwulfconf print` which will print the used `warewulf.conf`. If there is no valid + `warewulf.conf` a valid configuration is provided, prefilled with default values + and an IP configuration derived from the network configuration of the host +- All paths can now be configured in `warewulf.conf`, check the paths section of of + `wwctl --emptyconf genconfig warewulfconf print` for the available paths. +- Added experimental dnsmasq support. + +- new subcommand `wwctl genconf` is available with following subcommands: + * `completions` which will create the files used for bash-completion. Also + fish an zsh completions can be generated + * `defaults` which will generate a valid `defaults.conf` + * `man` which will generate the man pages in the specified directory + * `reference` which will generate a reference documentation for the wwctl commands + * `warwulfconf print` which will print the used `warewulf.conf`. If there is no valid + `warewulf.conf` a valid configuration is provided, prefilled with default values + and an IP configuration derived from the network configuration of the host +- All paths can now be configured in `warewulf.conf`, check the paths section of of + `wwctl --emptyconf genconfig warewulfconf print` for the available paths. +- Added experimental dnsmasq support. +- Check for formal correct IP and MAC addresses for command line options and + when reading in the configurations + ## [4.4.0] 2023-01-18 ### Added diff --git a/Makefile b/Makefile index e973f9ce..36e36d98 100644 --- a/Makefile +++ b/Makefile @@ -228,10 +228,11 @@ wwclient: $(WWCLIENT_DEPS) -X 'github.com/hpcng/warewulf/internal/pkg/warewulfconf.ConfigFile=/etc/warewulf/warewulf.conf'" -o ../../wwclient man_pages: wwctl - install -d man_pages - ./wwctl --emptyconf genconfig man man_pages - cp docs/man/man5/*.5 ./man_pages/ - cd man_pages; for i in wwctl*1 *.5; do echo "Compressing manpage: $$i"; gzip --force $$i; done + @install -d man_pages + @./wwctl --emptyconf genconfig man man_pages + @cp docs/man/man5/*.5 ./man_pages/ + @echo -n "Compressing manpage: " + @cd man_pages; for i in wwctl*1 *.5; do gzip --force $$i; echo -n "$$i "; done; echo update_configuration: vendor cmd/update_configuration/update_configuration.go cd cmd/update_configuration && go build -ldflags="-X 'github.com/hpcng/warewulf/internal/pkg/warewulfconf.ConfigFile=./etc/warewulf.conf'\ diff --git a/internal/app/wwctl/node/add/main.go b/internal/app/wwctl/node/add/main.go index a396d5a8..94888e59 100644 --- a/internal/app/wwctl/node/add/main.go +++ b/internal/app/wwctl/node/add/main.go @@ -16,6 +16,12 @@ the required type is returned */ func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { + // run converters for different types + for _, c := range vars.converters { + if err := c(); err != nil { + return err + } + } // remove the default network as all network values are assigned // to this network if _, ok := vars.nodeConf.NetDevs["default"]; ok && vars.netName != "" { diff --git a/internal/app/wwctl/node/add/main_test.go b/internal/app/wwctl/node/add/main_test.go index 98c8e47e..8fa8ed35 100644 --- a/internal/app/wwctl/node/add/main_test.go +++ b/internal/app/wwctl/node/add/main_test.go @@ -16,6 +16,7 @@ func Test_Add(t *testing.T) { args []string wantErr bool stdout string + chkout bool outDb string }{ {name: "single node add", @@ -39,6 +40,51 @@ nodes: n01: profiles: - foo +`}, + {name: "single node add, discoverable true, explicit", + args: []string{"--discoverable=true", "n01"}, + wantErr: false, + stdout: "", + outDb: `WW_INTERNAL: 43 +nodeprofiles: {} +nodes: + n01: + discoverable: "true" + profiles: + - default +`}, + {name: "single node add, discoverable true with yes", + args: []string{"--discoverable=yes", "n01"}, + wantErr: false, + stdout: "", + outDb: `WW_INTERNAL: 43 +nodeprofiles: {} +nodes: + n01: + discoverable: "true" + profiles: + - default +`}, + {name: "single node add, discoverable wrong argument", + args: []string{"--discoverable=maybe", "n01"}, + wantErr: true, + stdout: "", + chkout: false, + outDb: `WW_INTERNAL: 43 +nodeprofiles: {} +nodes: {} +`}, + {name: "single node add, discoverable false", + args: []string{"--discoverable=false", "n01"}, + wantErr: false, + stdout: "", + outDb: `WW_INTERNAL: 43 +nodeprofiles: {} +nodes: + n01: + discoverable: "false" + profiles: + - default `}, {name: "single node add with Kernel args", args: []string{"--kernelargs=foo", "n01"}, @@ -94,6 +140,15 @@ nodes: network devices: default: ipaddr: 10.0.0.1 +`}, + {name: "single node with malformed ipaddr", + args: []string{"--ipaddr=10.0.1", "n01"}, + wantErr: true, + stdout: "", + chkout: false, + outDb: `WW_INTERNAL: 43 +nodeprofiles: {} +nodes: {} `}, {name: "three nodes with ipaddr", args: []string{"--ipaddr=10.10.0.1", "n[01-02,03]"}, @@ -212,7 +267,7 @@ WW_INTERNAL: 43 t.Errorf("DB dump is wrong, got:'%s'\nwant:'%s'", dump, tt.outDb) t.FailNow() } - if buf.String() != tt.stdout { + if tt.chkout && buf.String() != tt.stdout { t.Errorf("Got wrong output, got:'%s'\nwant:'%s'", buf.String(), tt.stdout) t.FailNow() } diff --git a/internal/app/wwctl/node/add/root.go b/internal/app/wwctl/node/add/root.go index c498187f..a74a4cf4 100644 --- a/internal/app/wwctl/node/add/root.go +++ b/internal/app/wwctl/node/add/root.go @@ -12,8 +12,9 @@ import ( // Holds the variables which are needed in CobraRunE type variables struct { - netName string - nodeConf node.NodeConf + netName string + nodeConf node.NodeConf + converters []func() error } // Returns the newly created command @@ -28,9 +29,8 @@ func GetCommand() *cobra.Command { RunE: CobraRunE(&vars), Args: cobra.MinimumNArgs(1), } - vars.nodeConf.CreateFlags(baseCmd, []string{"tagdel", "nettagdel", "ipmitagdel"}) + vars.converters = vars.nodeConf.CreateFlags(baseCmd, []string{"tagdel", "nettagdel", "ipmitagdel"}) baseCmd.PersistentFlags().StringVar(&vars.netName, "netname", "", "Set network name for network options") - // register the command line completions if err := baseCmd.RegisterFlagCompletionFunc("container", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { list, _ := container.ListSources() diff --git a/internal/app/wwctl/node/edit/main.go b/internal/app/wwctl/node/edit/main.go index b580251e..c21a786c 100644 --- a/internal/app/wwctl/node/edit/main.go +++ b/internal/app/wwctl/node/edit/main.go @@ -77,21 +77,39 @@ func CobraRunE(cmd *cobra.Command, args []string) error { // ignore error as only may occurs under strange circumstances buffer, _ := io.ReadAll(file) err = yaml.Unmarshal(buffer, modifiedNodeMap) - if err == nil { - nodeList := make([]string, len(nodeMap)) - i := 0 - for key := range nodeMap { - nodeList[i] = key - i++ - } - yes := apiutil.ConfirmationPrompt(fmt.Sprintf("Are you sure you want to modify %d nodes", len(modifiedNodeMap))) - if !yes { + if err != nil { + yes := apiutil.ConfirmationPrompt(fmt.Sprintf("Got following error on parsing: %s, Retry", err)) + if yes { + continue + } else { break } - err = apinode.NodeDelete(&wwapiv1.NodeDeleteParameter{ - NodeNames: nodeList, - Hash: nodeListMsg.Hash, - }) + } + var checkErrors []error + for nodeName, node := range modifiedNodeMap { + err = node.Check() + if err != nil { + checkErrors = append(checkErrors, fmt.Errorf("node: %s parse error: %s", nodeName, err)) + } + } + if len(checkErrors) != 0 { + yes := apiutil.ConfirmationPrompt(fmt.Sprintf("Got following error on parsing: %s, Retry", checkErrors)) + if yes { + continue + } else { + break + } + } + + nodeList := make([]string, len(nodeMap)) + i := 0 + for key := range nodeMap { + nodeList[i] = key + i++ + } + yes := apiutil.ConfirmationPrompt(fmt.Sprintf("Are you sure you want to modify %d nodes", len(modifiedNodeMap))) + if yes { + err = apinode.NodeDelete(&wwapiv1.NodeDeleteParameter{NodeNames: nodeList, Force: true}) if err != nil { wwlog.Error("Problem deleting nodes before modification: %s", err) return err @@ -107,11 +125,6 @@ func CobraRunE(cmd *cobra.Command, args []string) error { os.Exit(1) } break - } else { - yes := apiutil.ConfirmationPrompt(fmt.Sprintf("Got following error on parsing: %s, Retry", err)) - if !yes { - break - } } } else { break diff --git a/internal/app/wwctl/node/set/main.go b/internal/app/wwctl/node/set/main.go index 63dad15f..4bc44f21 100644 --- a/internal/app/wwctl/node/set/main.go +++ b/internal/app/wwctl/node/set/main.go @@ -13,6 +13,12 @@ import ( ) func CobraRunE(cmd *cobra.Command, args []string) (err error) { + // run converters for different types + for _, c := range Converters { + if err := c(); err != nil { + return err + } + } // remove the default network as the all network values are assigned // to this network if NetName != "default" { diff --git a/internal/app/wwctl/node/set/root.go b/internal/app/wwctl/node/set/root.go index 0e5e81cf..ac4c1f14 100644 --- a/internal/app/wwctl/node/set/root.go +++ b/internal/app/wwctl/node/set/root.go @@ -38,11 +38,12 @@ var ( SetYes bool SetForce bool NodeConf node.NodeConf + Converters []func() error ) func init() { NodeConf = node.NewConf() - NodeConf.CreateFlags(baseCmd, []string{}) + Converters = NodeConf.CreateFlags(baseCmd, []string{}) baseCmd.PersistentFlags().StringVarP(&SetNetDevDel, "netdel", "D", "", "Delete the node's network device") baseCmd.PersistentFlags().StringVar(&NetName, "netname", "default", "Set network name for network options") baseCmd.PersistentFlags().BoolVarP(&SetNodeAll, "all", "a", false, "Set all nodes") diff --git a/internal/app/wwctl/profile/add/main.go b/internal/app/wwctl/profile/add/main.go index 0260f232..dc23d8fc 100644 --- a/internal/app/wwctl/profile/add/main.go +++ b/internal/app/wwctl/profile/add/main.go @@ -14,6 +14,12 @@ import ( ) func CobraRunE(cmd *cobra.Command, args []string) (err error) { + // run converters for different types + for _, c := range Converters { + if err := c(); err != nil { + return err + } + } // remove the default network as the all network values are assigned // to this network if NetName != "" { diff --git a/internal/app/wwctl/profile/add/root.go b/internal/app/wwctl/profile/add/root.go index e3ad6d24..6edf403d 100644 --- a/internal/app/wwctl/profile/add/root.go +++ b/internal/app/wwctl/profile/add/root.go @@ -25,12 +25,13 @@ var ( SetForce bool NetName string ProfileConf node.NodeConf + Converters []func() error ) // GetRootCommand returns the root cobra.Command for the application. func GetCommand() *cobra.Command { ProfileConf = node.NewConf() - ProfileConf.CreateFlags(baseCmd, + Converters = ProfileConf.CreateFlags(baseCmd, []string{"ipaddr", "ipaddr6", "ipmiaddr", "profile"}) baseCmd.PersistentFlags().StringVar(&NetName, "netname", "", "Set network name for network options") // register the command line completions diff --git a/internal/app/wwctl/profile/edit/main.go b/internal/app/wwctl/profile/edit/main.go index 0bf144d4..cbe018b3 100644 --- a/internal/app/wwctl/profile/edit/main.go +++ b/internal/app/wwctl/profile/edit/main.go @@ -73,27 +73,46 @@ func CobraRunE(cmd *cobra.Command, args []string) error { sum2 := hex.EncodeToString(hasher.Sum(nil)) wwlog.Debug("Hashes are before %s and after %s\n", sum1, sum2) if sum1 != sum2 { - wwlog.Debug("Nodes were modified") + wwlog.Debug("Profiles were modified") modifiedProfileMap := make(map[string]*node.NodeConf) _, _ = file.Seek(0, 0) // ignore error as only may occurs under strange circumstances buffer, _ := io.ReadAll(file) err = yaml.Unmarshal(buffer, modifiedProfileMap) - if err == nil { - nodeList := make([]string, len(profileMap)) - i := 0 - for key := range profileMap { - nodeList[i] = key - i++ - } - yes := apiutil.ConfirmationPrompt(fmt.Sprintf("Are you sure you want to modify %d nodes", len(modifiedProfileMap))) - if !yes { + if err != nil { + yes := apiutil.ConfirmationPrompt(fmt.Sprintf("Got following error on parsing: %s, Retry", err)) + if yes { + continue + } else { break } - err = apiprofile.ProfileDelete(&wwapiv1.NodeDeleteParameter{ - NodeNames: nodeList, - Hash: profileListMsg.Hash, - }) + } + var checkErrors []error + for nodeName, node := range modifiedProfileMap { + err = node.Check() + if err != nil { + checkErrors = append(checkErrors, fmt.Errorf("profile: %s parse error: %s", nodeName, err)) + } + } + if len(checkErrors) != 0 { + yes := apiutil.ConfirmationPrompt(fmt.Sprintf("Got following error on parsing: %s, Retry", checkErrors)) + if yes { + continue + } else { + break + } + } + pList := make([]string, len(profileMap)) + i := 0 + for key := range profileMap { + pList[i] = key + i++ + } + yes := apiutil.ConfirmationPrompt(fmt.Sprintf("Are you sure you want to modify %d nodes", len(modifiedProfileMap))) + if yes { + err = apiprofile.ProfileDelete(&wwapiv1.NodeDeleteParameter{NodeNames: pList, Force: true}) + + if err != nil { wwlog.Verbose("Problem deleting nodes before modification %s") } @@ -108,11 +127,6 @@ func CobraRunE(cmd *cobra.Command, args []string) error { os.Exit(1) } break - } else { - yes := apiutil.ConfirmationPrompt(fmt.Sprintf("Got following error on parsing: %s, Retry", err)) - if !yes { - break - } } } else { break diff --git a/internal/app/wwctl/profile/set/root.go b/internal/app/wwctl/profile/set/root.go index f95cbb6c..0c1fd56b 100644 --- a/internal/app/wwctl/profile/set/root.go +++ b/internal/app/wwctl/profile/set/root.go @@ -38,11 +38,12 @@ var ( SetForce bool NetName string ProfileConf node.NodeConf + Converters []func() error ) func init() { ProfileConf = node.NewConf() - ProfileConf.CreateFlags(baseCmd, + Converters = ProfileConf.CreateFlags(baseCmd, []string{"ipaddr", "ipaddr6", "ipmiaddr", "profile"}) baseCmd.PersistentFlags().StringVar(&NetName, "netname", "default", "Set network name for network options") baseCmd.PersistentFlags().StringVarP(&SetNetDevDel, "netdel", "D", "", "Delete the node's network device") diff --git a/internal/pkg/api/node/edit.go b/internal/pkg/api/node/edit.go index ca3c6100..d453b170 100644 --- a/internal/pkg/api/node/edit.go +++ b/internal/pkg/api/node/edit.go @@ -62,6 +62,10 @@ func NodeAddFromYaml(nodeList *wwapiv1.NodeYaml) (err error) { return errors.Wrap(err, "Could not unmarshall Yaml: %s\n") } for nodeName, node := range nodeMap { + err = node.Check() + if err != nil { + return errors.Errorf("error on node %s: %s", nodeName, err) + } nodeDB.Nodes[nodeName] = node } err = nodeDB.Persist() diff --git a/internal/pkg/api/node/node.go b/internal/pkg/api/node/node.go index fbc3dd31..2c63d8db 100644 --- a/internal/pkg/api/node/node.go +++ b/internal/pkg/api/node/node.go @@ -54,6 +54,12 @@ func NodeAdd(nap *wwapiv1.NodeAddParameter) (err error) { // only key } // setting node from the received yaml + err = nodeConf.Check() + if err != nil { + err = fmt.Errorf("error on check of node %s: %s", n.Id.Get(), err) + return + + } n.SetFrom(&nodeConf) if netName != "" && nodeConf.NetDevs[netName].Ipaddr != "" { // if more nodes are added increment IPv4 address @@ -251,6 +257,12 @@ func NodeSetParameterCheck(set *wwapiv1.NodeSetParameter, console bool) (nodeDB wwlog.Error(fmt.Sprintf("%v", err.Error())) return } + err = nodeConf.Check() + if err != nil { + err = fmt.Errorf("error on check of node %s: %s", n.Id.Get(), err) + return + + } n.SetFrom(&nodeConf) if set.NetdevDelete != "" { if _, ok := n.NetDevs[set.NetdevDelete]; !ok { diff --git a/internal/pkg/node/checkconf.go b/internal/pkg/node/checkconf.go new file mode 100644 index 00000000..a60acdf0 --- /dev/null +++ b/internal/pkg/node/checkconf.go @@ -0,0 +1,97 @@ +package node + +import ( + "fmt" + "net" + "reflect" + "strconv" + "strings" + + "github.com/hpcng/warewulf/internal/pkg/util" +) + +/* +Checks if for NodeConf all values can be parsed according to their type. +*/ +func (nodeConf *NodeConf) Check() (err error) { + nodeInfoType := reflect.TypeOf(nodeConf) + nodeInfoVal := reflect.ValueOf(nodeConf) + // now iterate of every field + for i := 0; i < nodeInfoVal.Elem().NumField(); i++ { + //wwlog.Debug("checking field: %s type: %s", nodeInfoType.Elem().Field(i).Name, nodeInfoVal.Elem().Field(i).Type()) + if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.String { + newFmt, err := checker(nodeInfoVal.Elem().Field(i).Interface().(string), nodeInfoType.Elem().Field(i).Tag.Get("type")) + if err != nil { + return fmt.Errorf("field: %s value:%s err: %s", nodeInfoType.Elem().Field(i).Name, nodeInfoVal.Elem().Field(i).String(), err) + } else if newFmt != "" { + nodeInfoVal.Elem().Field(i).SetString(newFmt) + } + } else if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.Ptr && !nodeInfoVal.Elem().Field(i).IsNil() { + nestType := reflect.TypeOf(nodeInfoVal.Elem().Field(i).Interface()) + nestVal := reflect.ValueOf(nodeInfoVal.Elem().Field(i).Interface()) + for j := 0; j < nestType.Elem().NumField(); j++ { + if nestType.Elem().Field(j).Type.Kind() == reflect.String { + //wwlog.Debug("checking field: %s type: %s", nestType.Elem().Field(j).Name, nestType.Elem().Field(j).Tag.Get("type")) + newFmt, err := checker(nestVal.Elem().Field(j).Interface().(string), nestType.Elem().Field(j).Tag.Get("type")) + if err != nil { + return fmt.Errorf("field: %s value:%s err: %s", nestType.Elem().Field(j).Name, nestVal.Elem().Field(j).String(), err) + } else if newFmt != "" { + nestVal.Elem().Field(j).SetString(newFmt) + } + } + } + } else if nodeInfoType.Elem().Field(i).Type == reflect.TypeOf(map[string]*NetDevs(nil)) { + netMap := nodeInfoVal.Elem().Field(i).Interface().(map[string]*NetDevs) + for _, val := range netMap { + netType := reflect.TypeOf(val) + netVal := reflect.ValueOf(val) + for j := 0; j < netType.Elem().NumField(); j++ { + newFmt, err := checker(netVal.Elem().Field(j).String(), netType.Elem().Field(j).Tag.Get("type")) + if err != nil { + return fmt.Errorf("field: %s value:%s err: %s", netType.Elem().Field(j).Name, netVal.Elem().Field(j).String(), err) + } else if newFmt != "" { + netVal.Elem().Field(j).SetString(newFmt) + } + } + } + } + } + return nil +} + +func checker(value string, valType string) (niceValue string, err error) { + if valType == "" || value == "" || util.InSlice(GetUnsetVerbs(), value) { + return "", nil + } + //wwlog.Debug("checker: %s is %s", value, valType) + switch valType { + case "": + return "", nil + case "bool": + if strings.ToLower(value) == "yes" { + return "true", nil + } + if strings.ToLower(value) == "no" { + return "false", nil + } + myBool, err := strconv.ParseBool(value) + return strconv.FormatBool(myBool), err + case "IP": + if addr := net.ParseIP(value); addr == nil { + return "", fmt.Errorf("%s can't be parsed to ip address", value) + } else { + return addr.String(), nil + } + case "MAC": + if mac, err := net.ParseMAC(value); err != nil { + return "", fmt.Errorf("%s can't be parsed to MAC address: %s", value, err) + } else { + return mac.String(), nil + } + case "uint": + if _, err := strconv.ParseUint(value, 10, 64); err != nil { + return "", fmt.Errorf("%s is not a uint: %s", value, err) + } + } + return "", nil +} diff --git a/internal/pkg/node/constructors.go b/internal/pkg/node/constructors.go index 10ee86ad..578cb094 100644 --- a/internal/pkg/node/constructors.go +++ b/internal/pkg/node/constructors.go @@ -2,6 +2,7 @@ package node import ( "errors" + "fmt" "os" "path" "sort" @@ -71,6 +72,21 @@ func New() (NodeYaml, error) { if err != nil { return ret, err } + wwlog.Debug("Checking nodes for types") + for nodeName, node := range ret.Nodes { + err = node.Check() + if err != nil { + wwlog.Warn("node: %s parsing error: %s", nodeName, err) + return ret, err + } + } + for profileName, profile := range ret.NodeProfiles { + err = profile.Check() + if err != nil { + wwlog.Warn("node: %s parsing error: %s", profileName, err) + return ret, err + } + } wwlog.Debug("Returning node object") cachedDB = ret @@ -119,12 +135,13 @@ func (config *NodeYaml) FindAllNodes() ([]NodeInfo, error) { defData, err := os.ReadFile(DefaultConfig) if err != nil { wwlog.Verbose("Couldn't read DefaultConfig :%s\n", err) + wwlog.Verbose("Using building defaults") + defData = []byte(FallBackConf) } wwlog.Debug("Unmarshalling default config\n") err = yaml.Unmarshal(defData, &defConf) if err != nil { wwlog.Verbose("Couldn't unmarshall defaults from file :%s\n", err) - wwlog.Verbose("Using building defaults") err = yaml.Unmarshal([]byte(FallBackConf), &defConf) if err != nil { wwlog.Warn("Could not get any defaults") @@ -165,6 +182,10 @@ func (config *NodeYaml) FindAllNodes() ([]NodeInfo, error) { node.Tags[keyname] = key delete(node.Keys, keyname) } + err = node.Check() + if err != nil { + return nil, fmt.Errorf("node: %s check error: %s", nodename, err) + } n.SetFrom(node) // only now the netdevs start to exist so that default values can be set for _, netdev := range n.NetDevs { diff --git a/internal/pkg/node/datastructure.go b/internal/pkg/node/datastructure.go index cecd1db5..2c27c289 100644 --- a/internal/pkg/node/datastructure.go +++ b/internal/pkg/node/datastructure.go @@ -44,7 +44,7 @@ type NodeConf struct { Init string `yaml:"init,omitempty" lopt:"init" sopt:"i" comment:"Define the init process to boot the container"` Root string `yaml:"root,omitempty" lopt:"root" comment:"Define the rootfs" ` AssetKey string `yaml:"asset key,omitempty" lopt:"asset" comment:"Set the node's Asset tag (key)"` - Discoverable string `yaml:"discoverable,omitempty" lopt:"discoverable" comment:"Make discoverable in given network (yes/no)"` + Discoverable string `yaml:"discoverable,omitempty" lopt:"discoverable" sopt:"e" comment:"Make discoverable in given network (true/false)" type:"bool"` Profiles []string `yaml:"profiles,omitempty" lopt:"profile" sopt:"P" comment:"Set the node's profile members (comma separated)"` NetDevs map[string]*NetDevs `yaml:"network devices,omitempty"` Tags map[string]string `yaml:"tags,omitempty" lopt:"tagadd" comment:"base key"` @@ -56,12 +56,12 @@ type NodeConf struct { type IpmiConf struct { UserName string `yaml:"username,omitempty" lopt:"ipmiuser" comment:"Set the IPMI username"` Password string `yaml:"password,omitempty" lopt:"ipmipass" comment:"Set the IPMI password"` - Ipaddr string `yaml:"ipaddr,omitempty" lopt:"ipmiaddr" comment:"Set the IPMI IP address"` - Netmask string `yaml:"netmask,omitempty" lopt:"ipminetmask" comment:"Set the IPMI netmask"` + Ipaddr string `yaml:"ipaddr,omitempty" lopt:"ipmiaddr" comment:"Set the IPMI IP address" type:"IP"` + Netmask string `yaml:"netmask,omitempty" lopt:"ipminetmask" comment:"Set the IPMI netmask" type:"IP"` Port string `yaml:"port,omitempty" lopt:"ipmiport" comment:"Set the IPMI port"` - Gateway string `yaml:"gateway,omitempty" lopt:"ipmigateway" comment:"Set the IPMI gateway"` + Gateway string `yaml:"gateway,omitempty" lopt:"ipmigateway" comment:"Set the IPMI gateway" type:"IP"` Interface string `yaml:"interface,omitempty" lopt:"ipmiinterface" comment:"Set the node's IPMI interface (defaults: 'lan')"` - Write string `yaml:"write,omitempty" lopt:"ipmiwrite" comment:"Enable the write of impi configuration (yes/no)"` + Write string `yaml:"write,omitempty" lopt:"ipmiwrite" comment:"Enable the write of impi configuration (true/false)" type:"bool"` Tags map[string]string `yaml:"tags,omitempty" lopt:"ipmitagadd" comment:"add ipmitags"` TagsDel []string `yaml:"tagsdel,omitempty" lopt:"ipmitagdel" comment:"remove ipmitags"` // should not go to disk only to wire } @@ -73,16 +73,18 @@ type KernelConf struct { type NetDevs struct { Type string `yaml:"type,omitempty" lopt:"type" sopt:"T" comment:"Set device type of given network"` - OnBoot string `yaml:"onboot,omitempty" lopt:"onboot" comment:"Enable/disable network device (yes/no)"` + OnBoot string `yaml:"onboot,omitempty" lopt:"onboot" comment:"Enable/disable network device (true/false)" type:"bool"` Device string `yaml:"device,omitempty" lopt:"netdev" sopt:"N" comment:"Set the device for given network"` - Hwaddr string `yaml:"hwaddr,omitempty" lopt:"hwaddr" sopt:"H" comment:"Set the device's HW address for given network"` - Ipaddr string `yaml:"ipaddr,omitempty" comment:"IPv4 address in given network" sopt:"I" lopt:"ipaddr"` + Hwaddr string `yaml:"hwaddr,omitempty" lopt:"hwaddr" sopt:"H" comment:"Set the device's HW address for given network" type:"MAC"` + Ipaddr string `yaml:"ipaddr,omitempty" comment:"IPv4 address in given network" sopt:"I" lopt:"ipaddr" type:"IP"` IpCIDR string `yaml:"ipcidr,omitempty"` - Ipaddr6 string `yaml:"ip6addr,omitempty" lopt:"ipaddr6" comment:"IPv6 address"` + Ipaddr6 string `yaml:"ip6addr,omitempty" lopt:"ipaddr6" comment:"IPv6 address" type:"IP"` Prefix string `yaml:"prefix,omitempty"` - Netmask string `yaml:"netmask,omitempty" lopt:"netmask" sopt:"M" comment:"Set the networks netmask"` - Gateway string `yaml:"gateway,omitempty" lopt:"gateway" sopt:"G" comment:"Set the node's network device gateway"` - MTU string `yaml:"mtu,omitempty" lopt:"mtu" comment:"Set the mtu"` + Netmask string `yaml:"netmask,omitempty" lopt:"netmask" sopt:"M" comment:"Set the networks netmask" type:"IP"` + Gateway string `yaml:"gateway,omitempty" lopt:"gateway" sopt:"G" comment:"Set the node's network device gateway" type:"IP"` + MTU string `yaml:"mtu,omitempty" lopt:"mtu" comment:"Set the mtu" type:"uint"` + Primary string `yaml:"primary,omitempty" lopt:"primary" comment:"Enable/disable network device as primary (true/false)" type:"bool"` + Default string `yaml:"default,omitempty"` /* backward compatibility */ Tags map[string]string `yaml:"tags,omitempty" lopt:"nettagadd" comment:"network tags"` TagsDel []string `yaml:"tagsdel,omitempty" lopt:"nettagdel" comment:"delete network tags"` // should not go to disk only to wire } diff --git a/internal/pkg/node/flags.go b/internal/pkg/node/flags.go new file mode 100644 index 00000000..e15b769c --- /dev/null +++ b/internal/pkg/node/flags.go @@ -0,0 +1,240 @@ +package node + +import ( + "fmt" + "net" + "reflect" + "strconv" + "strings" + + "github.com/hpcng/warewulf/internal/pkg/util" + "github.com/spf13/cobra" +) + +/* +Create cmd line flags from the NodeConf fields. Returns a []func() where every function +must be called, as the commandline parser returns e.g. netip.IP objects which must be parsedf +back to strings. +*/ +func (nodeConf *NodeConf) CreateFlags(baseCmd *cobra.Command, excludeList []string) (converters []func() error) { + nodeInfoType := reflect.TypeOf(nodeConf) + nodeInfoVal := reflect.ValueOf(nodeConf) + // now iterate of every field + for i := 0; i < nodeInfoVal.Elem().NumField(); i++ { + if nodeInfoType.Elem().Field(i).Tag.Get("comment") != "" && + !util.InSlice(excludeList, nodeInfoType.Elem().Field(i).Tag.Get("lopt")) { + field := nodeInfoVal.Elem().Field(i) + converters = append(converters, createFlags(baseCmd, excludeList, nodeInfoType.Elem().Field(i), &field)...) + } else if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.Ptr { + nestType := reflect.TypeOf(nodeInfoVal.Elem().Field(i).Interface()) + nestVal := reflect.ValueOf(nodeInfoVal.Elem().Field(i).Interface()) + for j := 0; j < nestType.Elem().NumField(); j++ { + field := nestVal.Elem().Field(j) + converters = append(converters, createFlags(baseCmd, excludeList, nestType.Elem().Field(j), &field)...) + } + } else if nodeInfoType.Elem().Field(i).Type == reflect.TypeOf(map[string]*NetDevs(nil)) { + netMap := nodeInfoVal.Elem().Field(i).Interface().(map[string]*NetDevs) + // add a default network so that it can hold values + key := "default" + if len(netMap) == 0 { + netMap[key] = new(NetDevs) + } else { + for keyIt := range netMap { + key = keyIt + break + } + } + netType := reflect.TypeOf(netMap[key]) + netVal := reflect.ValueOf(netMap[key]) + for j := 0; j < netType.Elem().NumField(); j++ { + field := netVal.Elem().Field(j) + converters = append(converters, createFlags(baseCmd, excludeList, netType.Elem().Field(j), &field)...) + } + } + } + return converters +} + +/* +Helper function to create the different PerisitantFlags() for different types. +*/ +func createFlags(baseCmd *cobra.Command, excludeList []string, + myType reflect.StructField, myVal *reflect.Value) (converters []func() error) { + if myType.Tag.Get("lopt") != "" { + if myType.Type.Kind() == reflect.String { + ptr := myVal.Addr().Interface().(*string) + switch myType.Tag.Get("type") { + case "uint": + converters = append(converters, func() error { + if !util.InSlice(GetUnsetVerbs(), *ptr) && *ptr != "" { + _, err := strconv.ParseUint(myType.Tag.Get(*ptr), 10, 32) + if err != nil { + return err + } + } + return nil + }) + if myType.Tag.Get("sopt") != "" { + baseCmd.PersistentFlags().StringVarP(ptr, + myType.Tag.Get("lopt"), + myType.Tag.Get("sopt"), + myType.Tag.Get("default"), + myType.Tag.Get("comment")) + } else { + baseCmd.PersistentFlags().StringVar(ptr, + myType.Tag.Get("lopt"), + myType.Tag.Get("default"), + myType.Tag.Get("comment")) + } + case "bool": + /* + Can't use the bool var from pflag as we need the UNSET verbs to be passwd correctly + */ + converters = append(converters, func() error { + if !util.InSlice(GetUnsetVerbs(), *ptr) && *ptr != "" { + if strings.ToLower(*ptr) == "yes" { + *ptr = "true" + return nil + } + if strings.ToLower(*ptr) == "no" { + *ptr = "false" + return nil + } + val, err := strconv.ParseBool(*ptr) + if err != nil { + return fmt.Errorf("commandline option %s needs to be bool", myType.Tag.Get("lopt")) + } + *ptr = strconv.FormatBool(val) + } + return nil + }) + if myType.Tag.Get("sopt") != "" { + baseCmd.PersistentFlags().StringVarP(ptr, + myType.Tag.Get("lopt"), + myType.Tag.Get("sopt"), + "", + myType.Tag.Get("comment")) + } else { + baseCmd.PersistentFlags().StringVar(ptr, + myType.Tag.Get("lopt"), + "", + myType.Tag.Get("comment")) + } + baseCmd.PersistentFlags().Lookup(myType.Tag.Get("lopt")).NoOptDefVal = "true" + case "IP": + converters = append(converters, func() error { + if !util.InSlice(GetUnsetVerbs(), *ptr) && *ptr != "" { + ipval := net.ParseIP(*ptr) + if ipval == nil { + return fmt.Errorf("commandline option %s needs to be an IP address", myType.Tag.Get("lopt")) + } + *ptr = ipval.String() + } + return nil + }) + if myType.Tag.Get("sopt") != "" { + baseCmd.PersistentFlags().StringVarP(ptr, + myType.Tag.Get("lopt"), + myType.Tag.Get("sopt"), + myType.Tag.Get("default"), + myType.Tag.Get("comment")) + } else { + baseCmd.PersistentFlags().StringVar(ptr, + myType.Tag.Get("lopt"), + myType.Tag.Get("default"), + myType.Tag.Get("comment")) + } + case "IPMask": + defaultConv := net.ParseIP(myType.Tag.Get("default")).DefaultMask() + var valueRaw net.IPMask + converters = append(converters, func() error { + if valueRaw != nil { + *ptr = valueRaw.String() + return nil + } else { + return fmt.Errorf("could not parse %s to IP", valueRaw.String()) + } + }) + if myType.Tag.Get("sopt") != "" { + baseCmd.PersistentFlags().IPMaskVarP(&valueRaw, + myType.Tag.Get("lopt"), + myType.Tag.Get("sopt"), + defaultConv, + myType.Tag.Get("comment")) + } else { + baseCmd.PersistentFlags().IPMaskVar(&valueRaw, + myType.Tag.Get("lopt"), + defaultConv, + myType.Tag.Get("comment")) + } + case "MAC": + converters = append(converters, func() error { + if !util.InSlice(GetUnsetVerbs(), *ptr) && *ptr != "" { + myMac, err := net.ParseMAC(*ptr) + if err != nil { + return err + } + *ptr = myMac.String() + } + return nil + }) + if myType.Tag.Get("sopt") != "" { + baseCmd.PersistentFlags().StringVarP(ptr, + myType.Tag.Get("lopt"), + myType.Tag.Get("sopt"), + "", + myType.Tag.Get("comment")) + } else { + baseCmd.PersistentFlags().StringVar(ptr, + myType.Tag.Get("lopt"), + "", + myType.Tag.Get("comment")) + } + default: + if myType.Tag.Get("sopt") != "" { + baseCmd.PersistentFlags().StringVarP(ptr, + myType.Tag.Get("lopt"), + myType.Tag.Get("sopt"), + myType.Tag.Get("default"), + myType.Tag.Get("comment")) + } else { + baseCmd.PersistentFlags().StringVar(ptr, + myType.Tag.Get("lopt"), + myType.Tag.Get("default"), + myType.Tag.Get("comment")) + } + } + } else if myType.Type == reflect.TypeOf([]string{}) { + ptr := myVal.Addr().Interface().(*[]string) + if myType.Tag.Get("sopt") != "" { + baseCmd.PersistentFlags().StringSliceVarP(ptr, + myType.Tag.Get("lopt"), + myType.Tag.Get("sopt"), + []string{myType.Tag.Get("default")}, + myType.Tag.Get("comment")) + } else if !util.InSlice(excludeList, myType.Tag.Get("lopt")) { + baseCmd.PersistentFlags().StringSliceVar(ptr, + myType.Tag.Get("lopt"), + []string{myType.Tag.Get("default")}, + myType.Tag.Get("comment")) + + } + } else if myType.Type == reflect.TypeOf(map[string]string{}) { + ptr := myVal.Addr().Interface().(*map[string]string) + if myType.Tag.Get("sopt") != "" { + baseCmd.PersistentFlags().StringToStringVarP(ptr, + myType.Tag.Get("lopt"), + myType.Tag.Get("sopt"), + map[string]string{}, // empty default! + myType.Tag.Get("comment")) + } else if !util.InSlice(excludeList, myType.Tag.Get("lopt")) { + baseCmd.PersistentFlags().StringToStringVar(ptr, + myType.Tag.Get("lopt"), + map[string]string{}, // empty default! + myType.Tag.Get("comment")) + + } + } + } + return converters +} diff --git a/internal/pkg/node/methods.go b/internal/pkg/node/methods.go index 9f89255a..c3ed61e2 100644 --- a/internal/pkg/node/methods.go +++ b/internal/pkg/node/methods.go @@ -10,6 +10,10 @@ import ( "github.com/hpcng/warewulf/internal/pkg/wwlog" ) +func GetUnsetVerbs() []string { + return []string{"UNSET", "DELETE", "UNDEF", "undef", "--", "nil", "0.0.0.0"} +} + /********** * * Filters @@ -77,7 +81,7 @@ func (ent *Entry) Set(val string) { return } - if val == "UNDEF" || val == "DELETE" || val == "UNSET" || val == "--" || val == "nil" { + if util.InSlice(GetUnsetVerbs(), val) { wwlog.Debug("Removing value for %v", *ent) ent.value = []string{""} } else { @@ -102,7 +106,7 @@ func (ent *Entry) SetSlice(val []string) { } else if len(val) == 1 && val[0] == "" { // check also for an "empty" slice return } - if val[0] == "UNDEF" || val[0] == "DELETE" || val[0] == "UNSET" || val[0] == "--" { + if util.InSlice(GetUnsetVerbs(), val[0]) { ent.value = []string{} } else { ent.value = val @@ -352,8 +356,10 @@ Create an empty node NodeConf */ func NewConf() (nodeconf NodeConf) { nodeconf.Ipmi = new(IpmiConf) + nodeconf.Ipmi.Tags = map[string]string{} nodeconf.Kernel = new(KernelConf) nodeconf.NetDevs = make(map[string]*NetDevs) + nodeconf.Tags = map[string]string{} return nodeconf } @@ -362,11 +368,18 @@ Create an empty node NodeInfo */ func NewInfo() (nodeInfo NodeInfo) { nodeInfo.Ipmi = new(IpmiEntry) + nodeInfo.Ipmi.Tags = map[string]*Entry{} nodeInfo.Kernel = new(KernelEntry) nodeInfo.NetDevs = make(map[string]*NetDevEntry) + nodeInfo.Tags = make(map[string]*Entry) return nodeInfo } +func NewNetDevEntry() (netdev NetDevEntry) { + netdev.Tags = make(map[string]*Entry) + return +} + /* Get a entry by its name */ diff --git a/internal/pkg/node/transformer_test.go b/internal/pkg/node/transformer_test.go index 8742f2a3..204b44a4 100644 --- a/internal/pkg/node/transformer_test.go +++ b/internal/pkg/node/transformer_test.go @@ -2,36 +2,351 @@ package node import ( "reflect" + "strconv" "testing" + + "gopkg.in/yaml.v2" ) +func NewTransformerTestNode() NodeYaml { + var data = ` +nodeprofiles: + default: + comment: This profile is automatically included for each node + ipmi: + username: greg + profile2: + tags: + foo: foo profile2 + comment: Comment profile2 + ipmi: + tags: + foo: foo ipmi profile +nodes: + test_node1: + comment: Node Comment + profiles: + - default + network devices: + net0: + device: eth1 + discoverable: true + ipmi: + username: chris + tags: + baar: baar node1 + test_node2: + primary: net0 + profiles: + - default + - profile2 + network devices: + net0: + netmask: 1.1.1.1 + net1: + ipaddr: 1.2.3.4 + tags: + baar: baar node2 + test_node3: + profiles: + - profile2 + tags: + foo: foo node3 + foobaar: foobaar node3 + ipmi: + ipaddr: 1.1.1.1 + tags: + foo: foo ipmi node3 + ` + var ret NodeYaml + _ = yaml.Unmarshal([]byte(data), &ret) + return ret +} func Test_nodeYaml_SetFrom(t *testing.T) { - c, _ := NewTestNode() - singleNodeConf := c.Nodes["test_node"] - singleNodeInfo := NewInfo() - singleNodeInfo.SetFrom(singleNodeConf) - tests := []struct { + c := NewTransformerTestNode() + nodes, _ := c.FindAllNodes() + test_node1 := NewInfo() + test_node2 := NewInfo() + test_node3 := NewInfo() + test_node4 := NewInfo() + for _, n := range nodes { + if n.Id.Get() == "test_node1" { + test_node1 = n + } + if n.Id.Get() == "test_node2" { + test_node2 = n + } + if n.Id.Get() == "test_node3" { + test_node3 = n + } + } + getByNametests := []struct { name string arg string want string wantErr bool }{ - {"Right comment", "Comment", "Node Comment", false}, - {"FieldName", "comment", "NodeComment", true}, + {"GetByName: FieldValue", "Comment", "Node Comment", false}, + {"GetByName: FieldName", "comment", "NodeComment", true}, } - for _, tt := range tests { + for _, tt := range getByNametests { t.Run(tt.name, func(t *testing.T) { - got, err := GetByName(&singleNodeInfo, tt.arg) + got, err := GetByName(&test_node1, tt.arg) if (err != nil) != tt.wantErr { t.Errorf("GetByName(%s,%s) error = %v, wantErr %v", - reflect.TypeOf(singleNodeConf), tt.arg, err, tt.wantErr) + reflect.TypeOf(test_node1), tt.arg, err, tt.wantErr) return } if (got != tt.want) != tt.wantErr { t.Errorf("GetByName(%s,%s) got = %v, want = %v", - reflect.TypeOf(singleNodeConf), tt.arg, got, tt.want) + reflect.TypeOf(test_node1), tt.arg, got, tt.want) return } }) } + t.Run("Get() comment", func(t *testing.T) { + comment := test_node1.Comment.Get() + if comment != "Node Comment" { + t.Errorf("Get() returned wrong comment: %s", comment) + } + }) + t.Run("Get() profile comment", func(t *testing.T) { + comment := test_node2.Comment.Get() + if comment != "Comment profile2" { + t.Errorf("Get() returned wrong comment: %s", comment) + } + }) + t.Run("Get() default ipxe", func(t *testing.T) { + value := test_node1.Ipxe.Get() + if value != "default" { + t.Errorf("Get() returned wrong ipxe template: %s", value) + } + }) + t.Run("GetSlice() default profile", func(t *testing.T) { + value := test_node1.Profiles.GetSlice()[0] + if value != "default" { + t.Errorf("GetSlice() returned wrong profile: %s", value) + } + }) + t.Run("Get() default kernel args", func(t *testing.T) { + value := test_node1.Kernel.Args.Get() + if value != "quiet crashkernel=no vga=791 net.naming-scheme=v238" { + t.Errorf("Get() returned wrong kernel args: %s", value) + } + }) + t.Run("Get() default network mask", func(t *testing.T) { + value := test_node1.NetDevs["net0"].Netmask.Get() + if value != "255.255.255.0" { + t.Errorf("Get() returned wrong default netmask: %s", value) + } + }) + t.Run("Get() default network mask", func(t *testing.T) { + value := test_node2.NetDevs["net0"].Netmask.Get() + if value != "1.1.1.1" { + t.Errorf("Get() returned wrong default netmask: %s", value) + } + }) + t.Run("GetB() primary for single network", func(t *testing.T) { + value := test_node1.NetDevs["net0"].Primary.GetB() + if !value { + t.Errorf("GetB() returned wrong: %s", strconv.FormatBool(value)) + } + }) + t.Run("GetB() for primary with two networks", func(t *testing.T) { + value := test_node2.NetDevs["net0"].Primary.GetB() + if !value { + t.Errorf("GetB() returned wrong: %s", strconv.FormatBool(value)) + } + }) + t.Run("GetB() for primary with two networks, get secondary network", func(t *testing.T) { + value := test_node2.NetDevs["net1"].Primary.GetB() + if value { + t.Errorf("GetB() returned wrong: %s", strconv.FormatBool(value)) + } + }) + t.Run("GetB() default discoverable", func(t *testing.T) { + value := test_node1.Discoverable.GetB() + if !value { + t.Errorf("GetB() returned wrong: %s", strconv.FormatBool(value)) + } + }) + t.Run("GetB() default discoverable", func(t *testing.T) { + value := test_node2.Discoverable.GetB() + if value { + t.Errorf("GetB() returned wrong: %s", strconv.FormatBool(value)) + } + }) + t.Run("Get() ipmi user from profile", func(t *testing.T) { + value := test_node2.Ipmi.UserName.Get() + if value != "greg" { + t.Errorf("Get() returned wrong ipmi username: %s", value) + } + }) + t.Run("Get() ipmi user from node", func(t *testing.T) { + value := test_node1.Ipmi.UserName.Get() + if value != "chris" { + t.Errorf("Get() returned wrong ipmi username: %s", value) + } + }) + t.Run("Get() tag foo from profile, node does not have this tag", func(t *testing.T) { + value := test_node2.Tags["foo"].Get() + if value != "foo profile2" { + t.Errorf("Get() returned wrong tag for foo: %s", value) + } + }) + t.Run("Get() tag baar from node, node tag map is not overwritten", func(t *testing.T) { + value := test_node2.Tags["baar"].Get() + if value != "baar node2" { + t.Errorf("Get() returned wrong tag for foo: %s", value) + } + }) + t.Run("Get() tag foo from node, tag present in profile", func(t *testing.T) { + value := test_node3.Tags["foo"].Get() + if value != "foo node3" { + t.Errorf("Get() returned wrong tag for foo: %s", value) + } + }) + t.Run("Get() tag foobaar from node", func(t *testing.T) { + value := test_node3.Tags["foobaar"].Get() + if value != "foobaar node3" { + t.Errorf("Get() returned wrong tag for foo: %s", value) + } + }) + t.Run("Get() ipmitag foo from profile, node does not have this tag", func(t *testing.T) { + value := test_node3.Ipmi.Tags["foo"].Get() + if value != "foo ipmi node3" { + t.Errorf("Get() returned wrong tag for foo: %s", value) + } + }) + t.Run("Set() comment foo for empty node", func(t *testing.T) { + test_node4.Comment.Set("foo") + nodeConf := NewConf() + nodeConf.GetFrom(test_node4) + ymlByte, _ := yaml.Marshal(nodeConf) + wanted := `comment: foo +kernel: {} +ipmi: {} +` + if !(wanted == string(ymlByte)) { + t.Errorf("Got wrong yml, wanted:\n'%s'\nGot:\n'%s'", wanted, string(ymlByte)) + } + // have to remove the comment for further tests, as vscode + // can test single functions + test_node4.Comment.Set("UNDEF") + nodeConf.GetFrom(test_node4) + nodeConf.Flatten() + ymlByte, _ = yaml.Marshal(nodeConf) + wanted = `{} +` + if string(ymlByte) != wanted { + t.Errorf("Couldn't unset comment:\n'%s'\nwanted:\n'%s'", string(ymlByte), wanted) + } + }) + + t.Run("Set() ipmiuser foo for flattened empty node", func(t *testing.T) { + test_node4.Ipmi.UserName.Set("foo") + nodeConf := NewConf() + nodeConf.GetFrom(test_node4) + nodeConf.Flatten() + ymlByte, _ := yaml.Marshal(nodeConf) + wanted := `ipmi: + username: foo +` + if !(wanted == string(ymlByte)) { + t.Errorf("Got wrong yml, wanted:\n'%s'\nGot:\n'%s'", wanted, string(ymlByte)) + } + test_node4.Ipmi.Tags["foo"] = &Entry{} + test_node4.Ipmi.Tags["foo"].Set("baar") + nodeConf.GetFrom(test_node4) + nodeConf.Flatten() + ymlByte, _ = yaml.Marshal(nodeConf) + wanted = `ipmi: + username: foo + tags: + foo: baar +` + if !(wanted == string(ymlByte)) { + t.Errorf("Got wrong yml, wanted:\n'%s'\nGot:\n'%s'", wanted, string(ymlByte)) + } + test_node4.Ipmi.UserName.Set("UNSET") + delete(test_node4.Ipmi.Tags, "foo") + }) + t.Run("Set() kernelargs foo for flattened empty node", func(t *testing.T) { + test_node4.Kernel.Args.Set("foo") + nodeConf := NewConf() + nodeConf.GetFrom(test_node4) + nodeConf.Flatten() + ymlByte, _ := yaml.Marshal(nodeConf) + wanted := `kernel: + args: foo +` + if !(wanted == string(ymlByte)) { + t.Errorf("Got wrong yml, wanted:\n'%s'\nGot:\n'%s'", wanted, string(ymlByte)) + } + test_node4.Kernel.Args.Set("--") + }) + t.Run("Set() tag foo to bar for flattened empty node", func(t *testing.T) { + test_node4.Tags["foo"] = &Entry{} + test_node4.Tags["foo"].Set("baar") + nodeConf := NewConf() + nodeConf.GetFrom(test_node4) + nodeConf.Flatten() + ymlByte, _ := yaml.Marshal(nodeConf) + wanted := `tags: + foo: baar +` + if !(wanted == string(ymlByte)) { + t.Errorf("Got wrong yml, wanted:\n'%s'\nGot:\n'%s'", wanted, string(ymlByte)) + } + delete(test_node4.Tags, "foo") + nodeConf.GetFrom(test_node4) + nodeConf.Flatten() + ymlByte, _ = yaml.Marshal(nodeConf) + wanted = `{} +` + if string(ymlByte) != wanted { + t.Errorf("Couldn't remove tag'%s'", string(ymlByte)) + } + + }) + t.Run("Set() netdev foo with device name baar for flattened empty node", func(t *testing.T) { + netdev := NewNetDevEntry() + test_node4.NetDevs["foo"] = &netdev + test_node4.NetDevs["foo"].Device.Set("baar") + nodeConf := NewConf() + nodeConf.GetFrom(test_node4) + nodeConf.Flatten() + ymlByte, _ := yaml.Marshal(nodeConf) + wanted := `network devices: + foo: + device: baar +` + if !(wanted == string(ymlByte)) { + t.Errorf("Got wrong yml, wanted:\n'%s'\nGot:\n'%s'", wanted, string(ymlByte)) + } + test_node4.NetDevs["foo"].Tags["netfoo"] = &Entry{} + test_node4.NetDevs["foo"].Tags["netfoo"].Set("netbaar") + nodeConf.GetFrom(test_node4) + nodeConf.Flatten() + wanted = `network devices: + foo: + device: baar + tags: + netfoo: netbaar +` + ymlByte, _ = yaml.Marshal(nodeConf) + if string(ymlByte) != wanted { + t.Errorf("Couldn set nettag: '%s' got: '%s'", wanted, string(ymlByte)) + } + + delete(test_node4.NetDevs, "foo") + nodeConf.GetFrom(test_node4) + nodeConf.Flatten() + ymlByte, _ = yaml.Marshal(nodeConf) + wanted = `{} +` + if string(ymlByte) != wanted { + t.Errorf("Couldn't remove tag'%s'", string(ymlByte)) + } + }) } diff --git a/internal/pkg/node/transformers.go b/internal/pkg/node/transformers.go index 75960b25..9eceac5d 100644 --- a/internal/pkg/node/transformers.go +++ b/internal/pkg/node/transformers.go @@ -6,7 +6,6 @@ import ( "github.com/hpcng/warewulf/internal/pkg/util" "github.com/hpcng/warewulf/internal/pkg/wwlog" - "github.com/spf13/cobra" ) /* @@ -194,102 +193,6 @@ func (nodeConf *NodeConf) getterFrom(nodeInfo NodeInfo, } } -/* -Create cmd line flags from the NodeConf fields -*/ -func (nodeConf *NodeConf) CreateFlags(baseCmd *cobra.Command, excludeList []string) { - nodeInfoType := reflect.TypeOf(nodeConf) - nodeInfoVal := reflect.ValueOf(nodeConf) - // now iterate of every field - for i := 0; i < nodeInfoVal.Elem().NumField(); i++ { - if nodeInfoType.Elem().Field(i).Tag.Get("comment") != "" && - !util.InSlice(excludeList, nodeInfoType.Elem().Field(i).Tag.Get("lopt")) { - field := nodeInfoVal.Elem().Field(i) - createFlags(baseCmd, excludeList, nodeInfoType.Elem().Field(i), &field) - } else if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.Ptr { - nestType := reflect.TypeOf(nodeInfoVal.Elem().Field(i).Interface()) - nestVal := reflect.ValueOf(nodeInfoVal.Elem().Field(i).Interface()) - for j := 0; j < nestType.Elem().NumField(); j++ { - field := nestVal.Elem().Field(j) - createFlags(baseCmd, excludeList, nestType.Elem().Field(j), &field) - } - } else if nodeInfoType.Elem().Field(i).Type == reflect.TypeOf(map[string]*NetDevs(nil)) { - netMap := nodeInfoVal.Elem().Field(i).Interface().(map[string]*NetDevs) - // add a default network so that it can hold values - key := "default" - if len(netMap) == 0 { - netMap[key] = new(NetDevs) - } else { - for keyIt := range netMap { - key = keyIt - break - } - } - netType := reflect.TypeOf(netMap[key]) - netVal := reflect.ValueOf(netMap[key]) - for j := 0; j < netType.Elem().NumField(); j++ { - field := netVal.Elem().Field(j) - createFlags(baseCmd, excludeList, netType.Elem().Field(j), &field) - } - } - } -} - -/* -Helper function to create the different PerisitantFlags() for different types. -*/ -func createFlags(baseCmd *cobra.Command, excludeList []string, - myType reflect.StructField, myVal *reflect.Value) { - if myType.Tag.Get("lopt") != "" { - if myType.Type.Kind() == reflect.String { - ptr := myVal.Addr().Interface().(*string) - if myType.Tag.Get("sopt") != "" { - baseCmd.PersistentFlags().StringVarP(ptr, - myType.Tag.Get("lopt"), - myType.Tag.Get("sopt"), - myType.Tag.Get("default"), - myType.Tag.Get("comment")) - } else if !util.InSlice(excludeList, myType.Tag.Get("lopt")) { - baseCmd.PersistentFlags().StringVar(ptr, - myType.Tag.Get("lopt"), - myType.Tag.Get("default"), - myType.Tag.Get("comment")) - - } - } else if myType.Type == reflect.TypeOf([]string{}) { - ptr := myVal.Addr().Interface().(*[]string) - if myType.Tag.Get("sopt") != "" { - baseCmd.PersistentFlags().StringSliceVarP(ptr, - myType.Tag.Get("lopt"), - myType.Tag.Get("sopt"), - []string{myType.Tag.Get("default")}, - myType.Tag.Get("comment")) - } else if !util.InSlice(excludeList, myType.Tag.Get("lopt")) { - baseCmd.PersistentFlags().StringSliceVar(ptr, - myType.Tag.Get("lopt"), - []string{myType.Tag.Get("default")}, - myType.Tag.Get("comment")) - - } - } else if myType.Type == reflect.TypeOf(map[string]string{}) { - ptr := myVal.Addr().Interface().(*map[string]string) - if myType.Tag.Get("sopt") != "" { - baseCmd.PersistentFlags().StringToStringVarP(ptr, - myType.Tag.Get("lopt"), - myType.Tag.Get("sopt"), - map[string]string{}, // empty default! - myType.Tag.Get("comment")) - } else if !util.InSlice(excludeList, myType.Tag.Get("lopt")) { - baseCmd.PersistentFlags().StringToStringVar(ptr, - myType.Tag.Get("lopt"), - map[string]string{}, // empty default! - myType.Tag.Get("comment")) - - } - } - } -} - /* Populates all fields of NodeInfo with Set from the values of NodeConf. @@ -363,24 +266,28 @@ func (node *NodeInfo) setterFrom(n *NodeConf, nameArg string, } } else if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.Ptr && !valField.IsZero() { nestedInfoType := reflect.TypeOf(nodeInfoVal.Elem().Field(i).Interface()) - netstedInfoVal := reflect.ValueOf(nodeInfoVal.Elem().Field(i).Interface()) + nestedInfoVal := reflect.ValueOf(nodeInfoVal.Elem().Field(i).Interface()) nestedConfVal := reflect.ValueOf(valField.Interface()) for j := 0; j < nestedInfoType.Elem().NumField(); j++ { nestedVal := nestedConfVal.Elem().FieldByName(nestedInfoType.Elem().Field(j).Name) if nestedVal.IsValid() { - if netstedInfoVal.Elem().Field(j).Type() == reflect.TypeOf(Entry{}) { - setter(netstedInfoVal.Elem().Field(j).Addr().Interface().(*Entry), nestedVal.String(), nameArg) - } else { + if nestedInfoVal.Elem().Field(j).Type() == reflect.TypeOf(Entry{}) { + setter(nestedInfoVal.Elem().Field(j).Addr().Interface().(*Entry), nestedVal.String(), nameArg) + } else if nestedInfoVal.Elem().Field(j).Type() == reflect.TypeOf(map[string](*Entry){}) { confMap := nestedVal.Interface().(map[string]string) - if netstedInfoVal.Elem().Field(j).IsNil() { - newMap := make(map[string]*Entry) - mapPtr := (netstedInfoVal.Elem().Field(j).Addr().Interface()).(*map[string](*Entry)) - *mapPtr = newMap + if nestedInfoVal.Elem().Field(j).IsNil() { + ptr := nestedInfoVal.Elem().Field(j).Addr().Interface().(*map[string](*Entry)) + *ptr = make(map[string]*Entry) } + tagMap := nestedInfoVal.Elem().Field(j).Interface().(map[string](*Entry)) for key, val := range confMap { - entr := new(Entry) - setter(entr, val, nameArg) - (netstedInfoVal.Elem().Field(j).Interface()).(map[string](*Entry))[key] = entr + if entr, ok := tagMap[key]; ok { + setter(entr, val, nameArg) + } else { + entr := new(Entry) + tagMap[key] = entr + setter(entr, val, nameArg) + } } } } @@ -388,9 +295,17 @@ func (node *NodeInfo) setterFrom(n *NodeConf, nameArg string, } else if nodeInfoType.Elem().Field(i).Type == reflect.TypeOf(map[string](*Entry)(nil)) { confMap := valField.Interface().(map[string]string) for key, val := range confMap { - entr := new(Entry) - setter(entr, val, nameArg) - (nodeInfoVal.Elem().Field(i).Interface()).(map[string](*Entry))[key] = entr + tagMap := nodeInfoVal.Elem().Field(i).Interface().(map[string](*Entry)) + if nodeInfoVal.Elem().Field(i).IsNil() { + tagMap = make(map[string]*Entry) + } + if entr, ok := tagMap[key]; ok { + setter(entr, val, nameArg) + } else { + entr := new(Entry) + tagMap[key] = entr + setter(entr, val, nameArg) + } } } else if nodeInfoType.Elem().Field(i).Type == reflect.TypeOf(map[string](*NetDevEntry)(nil)) { netValMap := valField.Interface().(map[string](*NetDevs)) diff --git a/internal/pkg/node/util.go b/internal/pkg/node/util.go index c641376c..8470a82b 100644 --- a/internal/pkg/node/util.go +++ b/internal/pkg/node/util.go @@ -27,10 +27,11 @@ func (config *NodeYaml) FindByHwaddr(hwa string) (NodeInfo, error) { } func (config *NodeYaml) FindByIpaddr(ipaddr string) (NodeInfo, error) { - if net.ParseIP(ipaddr) == nil { + if addr := net.ParseIP(ipaddr); addr == nil { return NodeInfo{}, errors.New("invalid IP:" + ipaddr) + } else { + ipaddr = addr.String() } - var ret NodeInfo n, _ := config.FindAllNodes() diff --git a/internal/pkg/node/util_test.go b/internal/pkg/node/util_test.go index bdce3bf7..2519105e 100644 --- a/internal/pkg/node/util_test.go +++ b/internal/pkg/node/util_test.go @@ -6,7 +6,7 @@ import ( "gopkg.in/yaml.v2" ) -func NewTestNode() (NodeYaml, error) { +func NewUtilTestNode() (NodeYaml, error) { var data = ` nodeprofiles: default: @@ -49,7 +49,7 @@ nodes: } func Test_nodeYaml_FindByHwaddr(t *testing.T) { - c, _ := NewTestNode() + c, _ := NewUtilTestNode() //type fields struct { // NodeProfiles map[string]*NodeConf // Nodes map[string]*NodeConf @@ -90,7 +90,7 @@ func Test_nodeYaml_FindByHwaddr(t *testing.T) { } func Test_nodeYaml_FindByIpaddr(t *testing.T) { - c, _ := NewTestNode() + c, _ := NewUtilTestNode() type args struct { ipaddr string }