diff --git a/internal/app/wwctl/profile/delete/root.go b/internal/app/wwctl/profile/delete/root.go index a5cea9e1..806a8d68 100644 --- a/internal/app/wwctl/profile/delete/root.go +++ b/internal/app/wwctl/profile/delete/root.go @@ -1,15 +1,30 @@ package delete -import "github.com/spf13/cobra" +import ( + "github.com/hpcng/warewulf/internal/pkg/node" + "github.com/spf13/cobra" +) var ( baseCmd = &cobra.Command{ DisableFlagsInUseLine: true, - Use: "delete [OPTIONS] PROFILE", - Short: "Delete a node profile", - Long: "This command deletes the node PROFILE. You may use a pattern for PROFILE.", - RunE: CobraRunE, - Args: cobra.MinimumNArgs(1), + Use: "delete [OPTIONS] PROFILE", + Short: "Delete a node profile", + Long: "This command deletes the node PROFILE. You may use a pattern for PROFILE.", + RunE: CobraRunE, + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + nodeDB, _ := node.New() + profiles, _ := nodeDB.FindAllProfiles() + var p_names []string + for _, profile := range profiles { + p_names = append(p_names, profile.Id.Get()) + } + return p_names, cobra.ShellCompDirectiveNoFileComp + }, } SetYes bool ) diff --git a/internal/app/wwctl/profile/edit/main.go b/internal/app/wwctl/profile/edit/main.go new file mode 100644 index 00000000..a5694e5d --- /dev/null +++ b/internal/app/wwctl/profile/edit/main.go @@ -0,0 +1,115 @@ +package edit + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + + apiprofile "github.com/hpcng/warewulf/internal/pkg/api/profile" + "github.com/hpcng/warewulf/internal/pkg/api/routes/wwapiv1" + apiutil "github.com/hpcng/warewulf/internal/pkg/api/util" + "github.com/hpcng/warewulf/internal/pkg/node" + "github.com/hpcng/warewulf/internal/pkg/util" + "github.com/hpcng/warewulf/internal/pkg/wwlog" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" +) + +func CobraRunE(cmd *cobra.Command, args []string) error { + canWrite := apiutil.CanWriteConfig() + if !canWrite.CanWriteConfig { + wwlog.Error("Can't write to config exiting") + os.Exit(1) + } + editor := os.Getenv("EDITOR") + if editor == "" { + editor = "/bin/vi" + } + if len(args) == 0 { + args = append(args, ".*") + } + filterList := wwapiv1.NodeList{ + Output: args, + } + profileListMsg := apiprofile.FilteredProfiles(&filterList) + profileMap := make(map[string]*node.NodeConf) + // got proper yaml back + _ = yaml.Unmarshal([]byte(profileListMsg.NodeConfMapYaml), profileMap) + file, err := ioutil.TempFile("/tmp", "ww4ProfileEdit*.yaml") + if err != nil { + wwlog.Error("Could not create temp file:%s \n", err) + } + defer os.Remove(file.Name()) + nodeConf := node.NewConf() + yamlTemplate := nodeConf.UnmarshalConf([]string{"tagsdel"}) + for { + _ = file.Truncate(0) + _, _ = file.Seek(0, 0) + if !NoHeader { + _, _ = file.WriteString("#profilename:\n# " + strings.Join(yamlTemplate, "\n# ") + "\n") + } + _, _ = file.WriteString(profileListMsg.NodeConfMapYaml) + _, _ = file.Seek(0, 0) + hasher := sha256.New() + if _, err := io.Copy(hasher, file); err != nil { + wwlog.Error("Problems getting checksum of file %s\n", err) + } + sum1 := hex.EncodeToString(hasher.Sum(nil)) + err = util.ExecInteractive(editor, file.Name()) + if err != nil { + wwlog.Error("Editor process existed with non-zero\n") + os.Exit(1) + } + _, _ = file.Seek(0, 0) + hasher.Reset() + if _, err := io.Copy(hasher, file); err != nil { + wwlog.Error("Problems getting checksum of file %s\n", err) + } + 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") + 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 { + break + } + err = apiprofile.ProfileDelete(&wwapiv1.NodeDeleteParameter{NodeNames: nodeList, Force: true}) + if err != nil { + wwlog.Verbose("Problem deleting nodes before modification %s") + } + buffer, _ = yaml.Marshal(modifiedProfileMap) + err = apiprofile.ProfileAddFromYaml(&wwapiv1.NodeYaml{NodeConfMapYaml: string(buffer)}) + if err != nil { + wwlog.Error("Got following problem when writing back yaml: %s", err) + os.Exit(1) + } + break + } else { + yes := apiutil.ConfirmationPrompt(fmt.Sprintf("Got following error on parsing: %s, Retry", err)) + if !yes { + break + } + } + } else { + break + } + } + + return nil +} diff --git a/internal/app/wwctl/profile/edit/root.go b/internal/app/wwctl/profile/edit/root.go new file mode 100644 index 00000000..6a073532 --- /dev/null +++ b/internal/app/wwctl/profile/edit/root.go @@ -0,0 +1,38 @@ +package edit + +import ( + "github.com/hpcng/warewulf/internal/pkg/node" + "github.com/spf13/cobra" +) + +var ( + baseCmd = &cobra.Command{ + DisableFlagsInUseLine: true, + Use: "edit [OPTIONS] NODENAME", + Short: "Edit node(s) with editor", + Long: "This command opens an editor for the given profiles.", + RunE: CobraRunE, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + nodeDB, _ := node.New() + profiles, _ := nodeDB.FindAllProfiles() + var p_names []string + for _, profile := range profiles { + p_names = append(p_names, profile.Id.Get()) + } + return p_names, cobra.ShellCompDirectiveNoFileComp + }, + } + NoHeader bool +) + +func init() { + baseCmd.PersistentFlags().BoolVar(&NoHeader, "noheader", false, "Do not print header") +} + +// GetRootCommand returns the root cobra.Command for the application. +func GetCommand() *cobra.Command { + return baseCmd +} diff --git a/internal/app/wwctl/profile/root.go b/internal/app/wwctl/profile/root.go index 2945d04e..fb2a805e 100644 --- a/internal/app/wwctl/profile/root.go +++ b/internal/app/wwctl/profile/root.go @@ -3,6 +3,7 @@ package profile import ( "github.com/hpcng/warewulf/internal/app/wwctl/profile/add" "github.com/hpcng/warewulf/internal/app/wwctl/profile/delete" + "github.com/hpcng/warewulf/internal/app/wwctl/profile/edit" "github.com/hpcng/warewulf/internal/app/wwctl/profile/list" "github.com/hpcng/warewulf/internal/app/wwctl/profile/set" "github.com/spf13/cobra" @@ -11,9 +12,9 @@ import ( var ( baseCmd = &cobra.Command{ DisableFlagsInUseLine: true, - Use: "profile COMMAND [OPTIONS]", - Short: "Node configuration profile management", - Long: "Management of node profile settings", + Use: "profile COMMAND [OPTIONS]", + Short: "Node configuration profile management", + Long: "Management of node profile settings", } ) @@ -22,6 +23,7 @@ func init() { baseCmd.AddCommand(set.GetCommand()) baseCmd.AddCommand(add.GetCommand()) baseCmd.AddCommand(delete.GetCommand()) + baseCmd.AddCommand(edit.GetCommand()) } // GetRootCommand returns the root cobra.Command for the application. diff --git a/internal/pkg/api/profile/delete.go b/internal/pkg/api/profile/delete.go new file mode 100644 index 00000000..e3c13185 --- /dev/null +++ b/internal/pkg/api/profile/delete.go @@ -0,0 +1,94 @@ +package apiprofile + +import ( + "fmt" + "os" + + "github.com/hpcng/warewulf/internal/pkg/api/routes/wwapiv1" + "github.com/hpcng/warewulf/internal/pkg/node" + "github.com/hpcng/warewulf/internal/pkg/warewulfd" + "github.com/hpcng/warewulf/internal/pkg/wwlog" + "github.com/hpcng/warewulf/pkg/hostlist" + "github.com/pkg/errors" +) + +// ProfileDelete adds profile deletion for management by Warewulf. +func ProfileDelete(ndp *wwapiv1.NodeDeleteParameter) (err error) { + + var profileList []node.NodeInfo + profileList, err = ProfileDeleteParameterCheck(ndp, false) + if err != nil { + return + } + + nodeDB, err := node.New() + if err != nil { + wwlog.Error("Failed to open node database: %s\n", err) + return + } + + for _, p := range profileList { + err := nodeDB.DelProfile(p.Id.Get()) + if err != nil { + wwlog.Error("%s\n", err) + } else { + //count++ + wwlog.Verbose("Deleting node: %s\n", p.Id.Print()) + } + } + + err = nodeDB.Persist() + if err != nil { + return errors.Wrap(err, "failed to persist nodedb") + } + + err = warewulfd.DaemonReload() + if err != nil { + return errors.Wrap(err, "failed to reload warewulf daemon") + } + return +} + +// ProfileDeleteParameterCheck does error checking on ProfileDeleteParameter. +// Output to the console if console is true. +// Returns the profiles to delete. +func ProfileDeleteParameterCheck(ndp *wwapiv1.NodeDeleteParameter, console bool) (profileList []node.NodeInfo, err error) { + + if ndp == nil { + err = fmt.Errorf("ProfileDeleteParameter is nil") + return + } + + nodeDB, err := node.New() + if err != nil { + wwlog.Error("Failed to open node database: %s\n", err) + return + } + + profiles, err := nodeDB.FindAllProfiles() + if err != nil { + wwlog.Error("Could not get node list: %s\n", err) + return + } + + node_args := hostlist.Expand(ndp.NodeNames) + + for _, r := range node_args { + var match bool + for _, p := range profiles { + if p.Id.Get() == r { + profileList = append(profileList, p) + match = true + } + } + + if !match { + fmt.Fprintf(os.Stderr, "ERROR: No match for node: %s\n", r) + } + } + + if len(profileList) == 0 { + fmt.Printf("No s found\n") + } + return +} diff --git a/internal/pkg/api/profile/edit.go b/internal/pkg/api/profile/edit.go new file mode 100644 index 00000000..5361f36c --- /dev/null +++ b/internal/pkg/api/profile/edit.go @@ -0,0 +1,70 @@ +package apiprofile + +import ( + "os" + + "github.com/hpcng/warewulf/internal/pkg/api/routes/wwapiv1" + "github.com/hpcng/warewulf/internal/pkg/node" + "github.com/hpcng/warewulf/internal/pkg/wwlog" + "github.com/pkg/errors" + "gopkg.in/yaml.v2" +) + +/* +Returns the nodes as a yaml string +*/ +func FindAllProfileConfs() *wwapiv1.NodeYaml { + nodeDB, err := node.New() + if err != nil { + wwlog.Error("Could not open nodeDB: %s\n", err) + os.Exit(1) + } + profileMap := nodeDB.NodeProfiles + // ignore err as nodeDB should always be correct + buffer, _ := yaml.Marshal(profileMap) + retVal := wwapiv1.NodeYaml{ + NodeConfMapYaml: string(buffer), + } + return &retVal +} + +/* +Returns filtered list of nodes +*/ +func FilteredProfiles(profileList *wwapiv1.NodeList) *wwapiv1.NodeYaml { + nodeDB, err := node.New() + if err != nil { + wwlog.Error("Could not open nodeDB: %s\n", err) + os.Exit(1) + } + profileMap := nodeDB.NodeProfiles + profileMap = node.FilterMapByName(profileMap, profileList.Output) + buffer, _ := yaml.Marshal(profileMap) + retVal := wwapiv1.NodeYaml{ + NodeConfMapYaml: string(buffer), + } + return &retVal +} + +/* +Add profiles from yaml +*/ +func ProfileAddFromYaml(nodeList *wwapiv1.NodeYaml) (err error) { + nodeDB, err := node.New() + if err != nil { + return errors.Wrap(err, "Could not open NodeDB: %s\n") + } + profileMap := make(map[string]*node.NodeConf) + err = yaml.Unmarshal([]byte(nodeList.NodeConfMapYaml), profileMap) + if err != nil { + return errors.Wrap(err, "Could not unmarshall Yaml: %s\n") + } + for profileName, profile := range profileMap { + nodeDB.NodeProfiles[profileName] = profile + } + err = nodeDB.Persist() + if err != nil { + return errors.Wrap(err, "failed to persist nodedb") + } + return nil +} diff --git a/internal/pkg/api/util/util.go b/internal/pkg/api/util/util.go index b9ebb5b1..bf34ec92 100644 --- a/internal/pkg/api/util/util.go +++ b/internal/pkg/api/util/util.go @@ -25,8 +25,10 @@ func ConfirmationPrompt(label string) (yes bool) { return } -func CanWriteConfig() (canwrite wwapiv1.CanWriteConfig) { - +/* +Simple check if the config can be written in case wwctl isn't run as root +*/ +func CanWriteConfig() (canwrite *wwapiv1.CanWriteConfig) { err := syscall.Access(node.ConfigFile, syscall.O_RDWR) if err != nil { wwlog.Warn("Couldn't open %s:%s", node.ConfigFile, err)