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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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]",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user