added remote resources

Signed-off-by: Christian Goll <cgoll@suse.com>
This commit is contained in:
Christian Goll
2024-11-21 17:36:10 +01:00
committed by Jonathon Anderson
parent c3833b5ba0
commit 5ff27061a3
22 changed files with 611 additions and 12 deletions

View File

@@ -25,6 +25,8 @@ func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) error {
if !node.ObjectIsEmpty(vars.nodeConf.NetDevs["UNDEF"]) {
netDev := *vars.nodeConf.NetDevs["UNDEF"]
vars.nodeConf.NetDevs[vars.nodeAdd.Net] = &netDev
vars.nodeConf.NetDevs[vars.nodeAdd.Net].Tags = vars.nodeAdd.NetTagsAdd
}
delete(vars.nodeConf.NetDevs, "UNDEF")
if vars.nodeAdd.FsName != "" {
@@ -50,6 +52,7 @@ func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) error {
return fmt.Errorf("partition and disk must be specified")
}
delete(vars.nodeConf.Disks, "UNDEF")
vars.nodeConf.Ipmi.Tags = vars.nodeAdd.IpmiTagsAdd
if len(vars.nodeConf.Profiles) == 0 {
vars.nodeConf.Profiles = []string{"default"}
}

View File

@@ -47,6 +47,7 @@ func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err err
return fmt.Errorf("partition and disk must be specified")
}
delete(vars.nodeConf.Disks, "UNDEF")
vars.nodeConf.Ipmi.Tags = vars.nodeAdd.IpmiTagsAdd
buffer, err := yaml.Marshal(vars.nodeConf)
if err != nil {
return fmt.Errorf("can not marshall nodeInfo: %s", err)

View File

@@ -18,32 +18,33 @@ func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err err
// to this network
if !node.ObjectIsEmpty(vars.profileConf.NetDevs["UNDEF"]) {
netDev := *vars.profileConf.NetDevs["UNDEF"]
vars.profileConf.NetDevs[vars.nodeAdd.Net] = &netDev
vars.profileConf.NetDevs[vars.profileAdd.Net] = &netDev
}
delete(vars.profileConf.NetDevs, "UNDEF")
if vars.nodeAdd.FsName != "" {
if !strings.HasPrefix(vars.nodeAdd.FsName, "/dev") {
if vars.nodeAdd.FsName == vars.nodeAdd.PartName {
vars.nodeAdd.FsName = "/dev/disk/by-partlabel/" + vars.nodeAdd.PartName
if vars.profileAdd.FsName != "" {
if !strings.HasPrefix(vars.profileAdd.FsName, "/dev") {
if vars.profileAdd.FsName == vars.profileAdd.PartName {
vars.profileAdd.FsName = "/dev/disk/by-partlabel/" + vars.profileAdd.PartName
} else {
return fmt.Errorf("filesystems need to have a underlying blockdev")
}
}
fs := *vars.profileConf.FileSystems["UNDEF"]
vars.profileConf.FileSystems[vars.nodeAdd.FsName] = &fs
vars.profileConf.FileSystems[vars.profileAdd.FsName] = &fs
}
delete(vars.profileConf.FileSystems, "UNDEF")
if vars.nodeAdd.DiskName != "" && vars.nodeAdd.PartName != "" {
if vars.profileAdd.DiskName != "" && vars.profileAdd.PartName != "" {
prt := *vars.profileConf.Disks["UNDEF"].Partitions["UNDEF"]
vars.profileConf.Disks["UNDEF"].Partitions[vars.nodeAdd.PartName] = &prt
vars.profileConf.Disks["UNDEF"].Partitions[vars.profileAdd.PartName] = &prt
delete(vars.profileConf.Disks["UNDEF"].Partitions, "UNDEF")
dsk := *vars.profileConf.Disks["UNDEF"]
vars.profileConf.Disks[vars.nodeAdd.DiskName] = &dsk
vars.profileConf.Disks[vars.profileAdd.DiskName] = &dsk
}
if (vars.nodeAdd.DiskName != "") != (vars.nodeAdd.PartName != "") {
if (vars.profileAdd.DiskName != "") != (vars.profileAdd.PartName != "") {
return fmt.Errorf("partition and disk must be specified")
}
delete(vars.profileConf.Disks, "UNDEF")
vars.profileConf.Ipmi.Tags = vars.profileAdd.IpmiTagsAdd
buffer, err := yaml.Marshal(vars.profileConf)
if err != nil {
return fmt.Errorf("can not marshall nodeInfo: %w", err)

View File

@@ -12,7 +12,7 @@ import (
type variables struct {
profileConf node.Profile
nodeAdd node.NodeConfAdd
profileAdd node.NodeConfAdd
}
// GetRootCommand returns the root cobra.Command for the application.
@@ -29,7 +29,7 @@ func GetCommand() *cobra.Command {
Args: cobra.ExactArgs(1),
}
vars.profileConf.CreateFlags(baseCmd)
vars.nodeAdd.CreateAddFlags(baseCmd)
vars.profileAdd.CreateAddFlags(baseCmd)
// register the command line completions
if err := baseCmd.RegisterFlagCompletionFunc("container", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, _ := container.ListSources()

View File

@@ -47,6 +47,7 @@ func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err err
return fmt.Errorf("partition and disk must be specified")
}
delete(vars.profileConf.Disks, "UNDEF")
vars.profileConf.Ipmi.Tags = vars.profileAdd.IpmiTagsAdd
buffer, err := yaml.Marshal(vars.profileConf)
if err != nil {
return fmt.Errorf("can not marshall nodeInfo: %s", err)

View File

@@ -0,0 +1,31 @@
package add
import (
"fmt"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/spf13/cobra"
)
func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) {
return func(cmd *cobra.Command, args []string) (err error) {
nodeYml, err := node.New()
if err != nil {
return fmt.Errorf("failed to load node configuration: %s", err)
}
if ok := nodeYml.Resource[args[0]] != nil; ok {
return fmt.Errorf("resource %s already exists", args[0])
}
if nodeYml.Resource == nil {
nodeYml.Resource = make(map[string]*node.RemoteRes)
}
res := node.RemoteRes{}
for key, val := range vars.tags {
res[key] = val
}
nodeYml.Resource[args[0]] = &res
err = nodeYml.Persist()
return err
}
}

View File

@@ -0,0 +1,58 @@
package add
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
)
func Test_resource_add(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
stdout string
inDB string
outDb string
}{{
name: "add resource",
args: []string{"--restagadd", "foo=baar", "test"},
wantErr: false,
stdout: "",
inDB: `nodeprofiles: {}
nodes: {}
`,
outDb: `nodeprofiles: {}
nodes: {}
resource:
test:
foo: baar
`}}
env := testenv.New(t)
defer env.RemoveAll(t)
warewulfd.SetNoDaemon()
for _, tt := range tests {
env.WriteFile(t, "etc/warewulf/nodes.conf", tt.inDB)
t.Run(tt.name, func(t *testing.T) {
baseCmd := GetCommand()
baseCmd.SetArgs(tt.args)
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
err := baseCmd.Execute()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, buf.String(), tt.stdout)
content := env.ReadFile(t, "etc/warewulf/nodes.conf")
assert.YAMLEq(t, tt.outDb, content)
}
})
}
}

View File

@@ -0,0 +1,25 @@
package add
import (
"github.com/spf13/cobra"
)
type variables struct {
tags map[string]string
}
// GetRootCommand returns the root cobra.Command for the application.
func GetCommand() *cobra.Command {
vars := variables{}
baseCmd := &cobra.Command{
DisableFlagsInUseLine: true,
Use: "add PROFILE",
Short: "Add a new node profile",
Long: "This command adds a new named PROFILE.",
Aliases: []string{"new", "create"},
RunE: CobraRunE(&vars),
Args: cobra.ExactArgs(1),
}
baseCmd.PersistentFlags().StringToStringVar(&vars.tags, "restagadd", map[string]string{}, "add cluster wide resource tags")
return baseCmd
}

View File

@@ -0,0 +1,25 @@
package delete
import (
"fmt"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/spf13/cobra"
)
func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) {
return func(cmd *cobra.Command, args []string) (err error) {
nodeYml, err := node.New()
if err != nil {
return fmt.Errorf("failed to load node configuration: %s", err)
}
for _, res := range args {
if _, ok := nodeYml.Resource[res]; !ok {
return fmt.Errorf("resource %s does not exist", res)
}
delete(nodeYml.Resource, res)
}
return nodeYml.Persist()
}
}

View File

@@ -0,0 +1,63 @@
package delete
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
)
func Test_resource_set(t *testing.T) {
tests := []struct {
name string
wantErr bool
args []string
stdout string
inDB string
outDb string
}{
{
name: "delete resource",
wantErr: false,
stdout: "",
args: []string{"test1"},
inDB: `nodeprofiles: {}
nodes: {}
resource:
test1: {}
test2: {}
`,
outDb: `nodeprofiles: {}
nodes: {}
resource:
test2: {}
`},
}
env := testenv.New(t)
defer env.RemoveAll(t)
warewulfd.SetNoDaemon()
for _, tt := range tests {
env.WriteFile(t, "etc/warewulf/nodes.conf", tt.inDB)
t.Run(tt.name, func(t *testing.T) {
baseCmd := GetCommand()
baseCmd.SetArgs(tt.args)
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
err := baseCmd.Execute()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, buf.String(), tt.stdout)
content := env.ReadFile(t, "etc/warewulf/nodes.conf")
assert.YAMLEq(t, tt.outDb, content)
}
})
}
}

View File

@@ -0,0 +1,31 @@
package delete
import (
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/node"
)
type variables struct {
}
// GetRootCommand returns the root cobra.Command for the application.
func GetCommand() *cobra.Command {
vars := variables{}
baseCmd := &cobra.Command{
DisableFlagsInUseLine: true,
Use: "list [RESOURCE]",
Short: "list resource",
Long: "This list all values the named RESOURCE. If no resource is named a list of the resources is printed.",
Aliases: []string{"show", "ls"},
RunE: CobraRunE(&vars),
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
nodeDB, _ := node.New()
return nodeDB.ListAllResources(), cobra.ShellCompDirectiveNoFileComp
},
}
return baseCmd
}

View File

@@ -0,0 +1,44 @@
package list
import (
"fmt"
"github.com/warewulf/warewulf/internal/app/wwctl/table"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
"github.com/spf13/cobra"
)
func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) {
return func(cmd *cobra.Command, args []string) (err error) {
nodeYml, err := node.New()
if err != nil {
return fmt.Errorf("failed to load node configuration: %s", err)
}
var resList []string
if len(args) == 1 {
resList = []string{args[0]}
} else {
resList = nodeYml.ListAllResources()
}
for _, resname := range resList {
res, err := nodeYml.GetResource(resname)
if err != nil {
return err
}
if !vars.all {
wwlog.Info("%s", resname)
} else {
t := table.New(cmd.OutOrStdout())
t.AddHeader(resname, "key", "value")
for key, val := range res {
t.AddLine(resname, key, val)
}
t.Print()
}
}
return nil
}
}

View File

@@ -0,0 +1,74 @@
package list
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
func Test_resource_set(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
stdout string
inDB string
}{
{
name: "list resource",
args: []string{},
wantErr: false,
stdout: `test1
test2
`,
inDB: `nodeprofiles: {}
nodes: {}
resource:
test1: {}
test2: {}
`,
},
{
name: "list a existing resource",
args: []string{"-a"},
wantErr: false,
stdout: `test key value
---- --- -----
test foo baar
`,
inDB: `nodeprofiles: {}
nodes: {}
resource:
test:
foo: baar
`,
},
}
env := testenv.New(t)
defer env.RemoveAll(t)
warewulfd.SetNoDaemon()
for _, tt := range tests {
env.WriteFile(t, "etc/warewulf/nodes.conf", tt.inDB)
t.Run(tt.name, func(t *testing.T) {
baseCmd := GetCommand()
baseCmd.SetArgs(tt.args)
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
wwlog.SetLogWriter(buf)
err := baseCmd.Execute()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, buf.String(), tt.stdout)
}
})
}
}

View File

@@ -0,0 +1,33 @@
package list
import (
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/node"
)
type variables struct {
all bool
}
// GetRootCommand returns the root cobra.Command for the application.
func GetCommand() *cobra.Command {
vars := variables{}
baseCmd := &cobra.Command{
DisableFlagsInUseLine: true,
Use: "list [RESOURCE]",
Short: "list resource",
Long: "This list all values the named RESOURCE. If no resource is named a list of the resources is printed.",
Aliases: []string{"show", "ls"},
RunE: CobraRunE(&vars),
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
nodeDB, _ := node.New()
return nodeDB.ListAllResources(), cobra.ShellCompDirectiveNoFileComp
},
}
baseCmd.PersistentFlags().BoolVarP(&vars.all, "all", "a", false, "Show all resources")
return baseCmd
}

View File

@@ -0,0 +1,30 @@
package resource
import (
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/app/wwctl/resource/add"
"github.com/warewulf/warewulf/internal/app/wwctl/resource/delete"
"github.com/warewulf/warewulf/internal/app/wwctl/resource/list"
"github.com/warewulf/warewulf/internal/app/wwctl/resource/set"
)
var (
baseCmd = &cobra.Command{
DisableFlagsInUseLine: true,
Use: "resource COMMAND [OPTIONS]",
Short: "manage resources",
Long: "Management of global resources",
}
)
func init() {
baseCmd.AddCommand(list.GetCommand())
baseCmd.AddCommand(set.GetCommand())
baseCmd.AddCommand(add.GetCommand())
baseCmd.AddCommand(delete.GetCommand())
}
// GetRootCommand returns the root cobra.Command for the application.
func GetCommand() *cobra.Command {
return baseCmd
}

View File

@@ -0,0 +1,31 @@
package set
import (
"fmt"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/spf13/cobra"
)
func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) {
return func(cmd *cobra.Command, args []string) (err error) {
nodeYml, err := node.New()
if err != nil {
return fmt.Errorf("failed to load node configuration: %s", err)
}
if ok := nodeYml.Resource[args[0]] != nil; ok {
if nodeYml.Resource == nil {
nodeYml.Resource = make(map[string]*node.RemoteRes)
}
res := *nodeYml.Resource[args[0]]
for key, val := range vars.tags {
res[key] = val
}
err = nodeYml.Persist()
return err
} else {
return fmt.Errorf("resource %s does not exist", args[0])
}
}
}

View File

@@ -0,0 +1,75 @@
package set
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
)
func Test_resource_set(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
stdout string
inDB string
outDb string
}{
{
name: "set resource",
args: []string{"--tagset", "foo=test", "test"},
wantErr: false,
stdout: "",
inDB: `nodeprofiles: {}
nodes: {}
resource:
test:
foo: baar
`,
outDb: `nodeprofiles: {}
nodes: {}
resource:
test:
foo: test
`},
{
name: "set non exiting resource",
args: []string{"--tagset", "foo=test", "none"},
wantErr: true,
stdout: "",
inDB: `nodeprofiles: {}
nodes: {}
resource:
test:
foo: baar
`,
outDb: ``},
}
env := testenv.New(t)
defer env.RemoveAll(t)
warewulfd.SetNoDaemon()
for _, tt := range tests {
env.WriteFile(t, "etc/warewulf/nodes.conf", tt.inDB)
t.Run(tt.name, func(t *testing.T) {
baseCmd := GetCommand()
baseCmd.SetArgs(tt.args)
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
err := baseCmd.Execute()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, buf.String(), tt.stdout)
content := env.ReadFile(t, "etc/warewulf/nodes.conf")
assert.YAMLEq(t, tt.outDb, content)
}
})
}
}

View File

@@ -0,0 +1,33 @@
package set
import (
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/node"
)
type variables struct {
tags map[string]string
}
// GetRootCommand returns the root cobra.Command for the application.
func GetCommand() *cobra.Command {
vars := variables{}
baseCmd := &cobra.Command{
DisableFlagsInUseLine: true,
Use: "set RESOURCE",
Short: "change a resource",
Long: "This command changes the named RESOURCE. Use UNDEF to remove a tag.",
Aliases: []string{"modify", "change"},
RunE: CobraRunE(&vars),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
nodeDB, _ := node.New()
return nodeDB.ListAllResources(), cobra.ShellCompDirectiveNoFileComp
},
}
baseCmd.PersistentFlags().StringToStringVar(&vars.tags, "tagset", map[string]string{}, "set cluster wide resource tags")
return baseCmd
}

View File

@@ -183,6 +183,18 @@ func (config *NodesYaml) ListAllProfiles() []string {
return nodeList
}
/*
Return the names of all available remote resources
*/
func (config *NodesYaml) ListAllResources() []string {
var resList []string
for name := range config.Resource {
resList = append(resList, name)
}
sort.Strings(resList)
return resList
}
/*
FindDiscoverableNode returns the first discoverable node and an
interface to associate with the discovered interface. If the nodUNDEFe has
@@ -211,3 +223,13 @@ func (config *NodesYaml) FindDiscoverableNode() (Node, string, error) {
return EmptyNode(), "", ErrNoUnconfigured
}
/*
get the given resource
*/
func (config *NodesYaml) GetResource(id string) (res RemoteRes, err error) {
if found, ok := config.Resource[id]; ok {
return *found, nil
}
return res, ErrNotFound
}

View File

@@ -17,6 +17,7 @@ Structure of which goes to disk
type NodesYaml struct {
NodeProfiles map[string]*Profile
Nodes map[string]*Node
Resource map[string]*RemoteRes `yaml:"resource,omitempty"`
}
/*
@@ -53,6 +54,8 @@ type Profile struct {
PrimaryNetDev string `yaml:"primary network,omitempty" lopt:"primarynet" sopt:"p" comment:"Set the primary network interface"`
Disks map[string]*Disk `yaml:"disks,omitempty"`
FileSystems map[string]*FileSystem `yaml:"filesystems,omitempty"`
resource map[string]*RemoteRes `yaml:"omitempty"`
Resources []string `yaml:"resources,omitempty" lopt:"resources" comment:"Set the resources available to the node or profile"`
}
type IpmiConf struct {
@@ -124,3 +127,14 @@ type FileSystem struct {
Options []string `yaml:"options,omitempty" comment:"any additional options to be passed to the format-specific mkfs utility"`
MountOptions string `yaml:"mount_options,omitempty" comment:"any special options to be passed to the mount command"`
}
/*
Definition of any remote. Can be a nfs share or other FS types
*/
type RemoteRes map[string]string
// always return so that the resource isn't marshalled
// func (res RemoteRes) IsZero() bool {
// return true
// }

View File

@@ -53,6 +53,9 @@ func recursiveCreateFlags(obj interface{}, baseCmd *cobra.Command) {
nodeInfoType := reflect.TypeOf(obj)
nodeInfoVal := reflect.ValueOf(obj)
for i := 0; i < nodeInfoVal.Elem().NumField(); i++ {
if !nodeInfoType.Elem().Field(i).IsExported() {
continue
}
if nodeInfoType.Elem().Field(i).Tag.Get("comment") != "" {
field := nodeInfoVal.Elem().Field(i)
createFlags(baseCmd, nodeInfoType.Elem().Field(i), &field)

View File

@@ -103,6 +103,7 @@ func EmptyNode() (nodeconf Node) {
nodeconf.Ipmi.Tags = map[string]string{}
nodeconf.Kernel = new(KernelConf)
nodeconf.NetDevs = make(map[string]*NetDev)
nodeconf.resource = make(map[string]*RemoteRes)
nodeconf.Tags = map[string]string{}
return nodeconf
}