Recommended changes from review of #1568

- Make Resources an `interface{}` to support arbitrary yaml
- Remove `wwctl resource` as incompatible with arbitrary yaml
- Revert changes to host overlay templates
- Remove NFS mount options from warewulf.conf
- Replace NFS support / resource prefix with "fstab" resource
- Move resources to profiles
- Migrate warewulf.conf mounts to nodes.conf with `wwctl upgrade`
- Add documentation

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Jonathon Anderson
2025-01-17 01:28:14 -07:00
parent eff1edd038
commit bc52f3b7d7
61 changed files with 344 additions and 843 deletions

View File

@@ -183,18 +183,6 @@ 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.Resources {
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
@@ -223,13 +211,3 @@ func (config *NodesYaml) FindDiscoverableNode() (Node, string, error) {
return EmptyNode(), "", ErrNoUnconfigured
}
/*
get the given resource
*/
func (config *NodesYaml) GetResource(id string) (res Resource, err error) {
if found, ok := config.Resources[id]; ok {
return found, nil
}
return res, ErrNotFound
}

View File

@@ -17,13 +17,14 @@ nodeprofiles:
override:
device: ib0
type: profile
resources:
fstab:
- file: /home
nodes:
test_node1:
network devices:
net0:
device: eth0
resources:
- NFSHOME
test_node2:
primary network: net1
network devices:
@@ -53,9 +54,6 @@ nodes:
network devices:
override:
device: ib1
resources:
NFSHOME:
mountpoint: /home
`
var ret NodesYaml
err := yaml.Unmarshal([]byte(data), &ret)
@@ -122,12 +120,6 @@ func Test_Primary_Network(t *testing.T) {
assert.Equal(t, "ib1", test_node6.NetDevs["override"].Device)
assert.Equal(t, "profile", test_node6.NetDevs["override"].Type)
})
t.Run("resource is defined", func(t *testing.T) {
assert.Contains(t, test_node1.Resources, "NFSHOME")
res, err := c.GetResource(test_node1.Resources[0])
assert.NoError(t, err)
assert.Contains(t, res, "mountpoint")
})
}
var findDiscoverableNodeTests = []struct {

View File

@@ -17,7 +17,6 @@ Structure of which goes to disk
type NodesYaml struct {
NodeProfiles map[string]*Profile `yaml:"nodeprofiles"`
Nodes map[string]*Node `yaml:"nodes"`
Resources map[string]Resource `yaml:"resources,omitempty"`
}
/*
@@ -54,7 +53,7 @@ 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"`
Resources []string `yaml:"resources,omitempty" lopt:"resources" comment:"Set the resources available to the node or profile"`
Resources map[string]Resource `yaml:"resources,omitempty"`
}
type IpmiConf struct {
@@ -127,4 +126,4 @@ type FileSystem struct {
MountOptions string `yaml:"mount_options,omitempty" comment:"any special options to be passed to the mount command"`
}
type Resource map[string]string
type Resource interface{}

View File

@@ -103,6 +103,9 @@ func Test_listFields(t *testing.T) {
},
},
},
Resources: map[string]Resource{
"resource": "resvalue",
},
},
},
fields: []string{
@@ -142,7 +145,7 @@ func Test_listFields(t *testing.T) {
"NetDevs[default].Tags[nettag]",
"Tags[tag]",
"PrimaryNetDev",
"Resources",
"Resources[resource]",
},
},
"profile": {
@@ -157,6 +160,9 @@ func Test_listFields(t *testing.T) {
},
},
},
Resources: map[string]Resource{
"resource": "resvalue",
},
},
fields: []string{
"Profiles",
@@ -193,7 +199,7 @@ func Test_listFields(t *testing.T) {
"NetDevs[default].Tags[nettag]",
"Tags[tag]",
"PrimaryNetDev",
"Resources",
"Resources[resource]",
},
},
}

View File

@@ -49,38 +49,47 @@ func (add *NodeConfAdd) CreateAddFlags(baseCmd *cobra.Command) {
}
func recursiveCreateFlags(obj interface{}, baseCmd *cobra.Command) {
// now iterate of every field
nodeInfoType := reflect.TypeOf(obj)
nodeInfoVal := reflect.ValueOf(obj)
for i := 0; i < nodeInfoVal.Elem().NumField(); i++ {
if !nodeInfoType.Elem().Field(i).IsExported() {
elemType := reflect.TypeOf(obj).Elem()
elemVal := reflect.ValueOf(obj).Elem()
for i := 0; i < elemVal.NumField(); i++ {
field := elemType.Field(i)
fieldVal := elemVal.Field(i)
if !field.IsExported() {
continue
}
if nodeInfoType.Elem().Field(i).Tag.Get("comment") != "" {
field := nodeInfoVal.Elem().Field(i)
createFlags(baseCmd, nodeInfoType.Elem().Field(i), &field)
} else if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.Ptr {
recursiveCreateFlags(nodeInfoVal.Elem().Field(i).Interface(), baseCmd)
if field.Tag.Get("comment") != "" {
createFlags(baseCmd, field, &fieldVal)
} else if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.Map &&
nodeInfoType.Elem().Field(i).Type != reflect.TypeOf(map[string]string{}) {
// add a map with key UNDEF so that it can hold values N.B. UNDEF can never be added through command line
key := reflect.ValueOf("UNDEF")
if nodeInfoVal.Elem().Field(i).Len() == 0 {
if nodeInfoVal.Elem().Field(i).IsNil() {
nodeInfoVal.Elem().Field(i).Set(reflect.MakeMap(nodeInfoType.Elem().Field(i).Type))
} else if field.Anonymous {
recursiveCreateFlags(fieldVal.Addr().Interface(), baseCmd)
} else if field.Type.Kind() == reflect.Ptr {
recursiveCreateFlags(fieldVal.Interface(), baseCmd)
} else if field.Type.Kind() == reflect.Struct {
recursiveCreateFlags(fieldVal.Addr().Interface(), baseCmd)
} else if field.Type.Kind() == reflect.Map {
switch field.Type.Elem().Kind() {
case reflect.String, reflect.Interface:
continue
case reflect.Pointer, reflect.Slice, reflect.Map:
// add a map with key UNDEF so that it can hold values N.B. UNDEF can never be added through command line
key := reflect.ValueOf("UNDEF")
if fieldVal.Len() == 0 {
if fieldVal.IsNil() {
fieldVal.Set(reflect.MakeMap(field.Type))
}
newPtr := reflect.New(field.Type.Elem().Elem())
fieldVal.SetMapIndex(key, newPtr)
} else {
key = fieldVal.MapKeys()[0]
}
newPtr := reflect.New(nodeInfoType.Elem().Field(i).Type.Elem().Elem())
nodeInfoVal.Elem().Field(i).SetMapIndex(key, newPtr)
} else {
key = nodeInfoVal.Elem().Field(i).MapKeys()[0]
recursiveCreateFlags(fieldVal.MapIndex(key).Interface(), baseCmd)
}
recursiveCreateFlags(nodeInfoVal.Elem().Field(i).MapIndex(key).Interface(), baseCmd)
} else if nodeInfoType.Elem().Field(i).Anonymous {
recursiveCreateFlags(nodeInfoVal.Elem().Field(i).Addr().Interface(), baseCmd)
} else if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.Struct {
recursiveCreateFlags(nodeInfoVal.Elem().Field(i).Addr().Interface(), baseCmd)
}
}
}

View File

@@ -1,43 +1,16 @@
package node
import (
"bytes"
"encoding/gob"
"reflect"
"strings"
"dario.cat/mergo"
"github.com/mohae/deepcopy"
"github.com/warewulf/warewulf/internal/pkg/util"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
// copyProfile creates a deep copy of the given Profile object.
// It uses encoding/gob to serialize and deserialize the Profile, ensuring
// that all nested fields are copied.
//
// Parameters:
// - original: The Profile object to be copied.
//
// Returns:
// - A new Profile object that is a deep copy of the input.
// - An error if serialization or deserialization fails.
func copyProfile(original Profile) (Profile, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
dec := gob.NewDecoder(&buf)
profile := Profile{}
if err := enc.Encode(original); err != nil {
return profile, err
} else {
if err := dec.Decode(&profile); err != nil {
return profile, err
} else {
return profile, nil
}
}
}
// getNodeProfiles retrieves a list of profile IDs associated with a specific node ID.
// It retrives nested profiles and ensures the list is cleaned of duplicates
// and negations (denoted with a '~' prefix).
@@ -85,6 +58,33 @@ func (config *NodesYaml) appendProfileProfiles(profiles []string, id string) []s
return profiles
}
type InterfaceTransformer struct{}
func (t InterfaceTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
if typ.Kind() == reflect.Interface {
return func(dst, src reflect.Value) error {
if !src.IsValid() || src.IsZero() {
return nil
}
// Handle merging of concrete values
switch src.Interface().(type) {
case map[string]interface{}:
if dst.IsNil() {
dst.Set(reflect.New(src.Elem().Type()).Elem())
}
return mergo.Merge(dst.Interface(), src.Interface(), mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(t))
case []interface{}:
dst.Set(src)
default:
dst.Set(src)
}
return nil
}
}
return nil
}
// MergeNode merges the configuration of a node identified by `id` with all the profiles
// associated with it, producing a fully composed `Node` and a `fieldMap` detailing the
// sources of various configuration fields.
@@ -121,11 +121,9 @@ func (config *NodesYaml) MergeNode(id string) (node Node, fields fieldMap, err e
if profile, err := config.GetProfile(profileID); err != nil {
wwlog.Warn("profile not found: %s", profileID)
continue
} else if profile, err := copyProfile(profile); err != nil {
wwlog.Warn("error processing profile %s: %v", profileID, err)
continue
} else {
if err = mergo.Merge(&node.Profile, profile, mergo.WithAppendSlice, mergo.WithOverride); err != nil {
profile := deepcopy.Copy(profile)
if err = mergo.Merge(&node.Profile, profile, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(InterfaceTransformer{})); err != nil {
return node, fields, err
}
for _, fieldName := range listFields(profile) {
@@ -150,8 +148,15 @@ func (config *NodesYaml) MergeNode(id string) (node Node, fields fieldMap, err e
if value, err := getNestedFieldValue(originalNode, fieldName); err == nil && valueStr(value) != "" {
source := ""
prevSource := fields.Source(fieldName)
if value.Kind() == reflect.Slice && prevSource != "" {
source = strings.Join([]string{prevSource, id}, ",")
if prevSource != "" {
switch value.Kind() {
case reflect.Slice:
source = strings.Join([]string{prevSource, id}, ",")
case reflect.Interface:
if _, ok := value.Interface().([]interface{}); ok {
source = strings.Join([]string{prevSource, id}, ",")
}
}
}
if value, err := getNestedFieldString(node, fieldName); err == nil {
fields.Set(fieldName, source, value)

View File

@@ -758,6 +758,30 @@ nodeprofiles:
"p2 netdev tag",
},
},
"resources": {
nodesConf: `
nodeprofiles:
p1:
resources:
fstab:
- spec: warewulf:/home
file: /home
vfstype: nfs
nodes:
n1:
profiles:
- p1
resources:
fstab:
- spec: warewulf:/opt
file: /opt
vfstype: nfs
`,
nodes: []string{"n1"},
fields: []string{"Resources[fstab]"},
sources: []string{"p1,n1"},
values: []string{"[map[file:/home spec:warewulf:/home vfstype:nfs] map[file:/opt spec:warewulf:/opt vfstype:nfs]]"},
},
}
for name, tt := range tests {
@@ -781,13 +805,13 @@ nodeprofiles:
}
var nodes []Node
for i, _ := range tt.nodes {
for i := range tt.nodes {
node, _, mergeErr := registry.MergeNode(tt.nodes[i])
assert.NoError(t, mergeErr)
nodes = append(nodes, node)
}
for i, _ := range tt.nodes {
for i := range tt.nodes {
_, fields, _ := registry.MergeNode(tt.nodes[i])
value, valueErr := getNestedFieldString(nodes[i], tt.fields[i])
assert.NoError(t, valueErr)

View File

@@ -154,13 +154,16 @@ func recursiveFlatten(obj interface{}) (hasContent bool) {
case reflect.Map:
mapIter := valObj.Elem().Field(i).MapRange()
for mapIter.Next() {
if mapIter.Value().Kind() == reflect.String {
if mapIter.Value().String() != "" {
hasContent = true
switch mapIter.Value().Kind() {
case reflect.Map, reflect.Pointer, reflect.Slice:
if mapIter.Value().Type().Elem().Kind() == reflect.Struct {
ret := recursiveFlatten(mapIter.Value().Interface())
hasContent = ret || hasContent
} else {
hasContent = !mapIter.Value().IsZero() || hasContent
}
} else {
ret := recursiveFlatten(mapIter.Value().Interface())
hasContent = ret || hasContent
default:
hasContent = !mapIter.Value().IsZero() || hasContent
}
}