Before #1628, all default constructors for nodes and profiles attempted to construct objects with pre-populated empty fields (e.g., for pointers to member structs and for collections that default to nil). Changes to MergeNode in #1629 changed this behavior intentionally to simplify the generation of minimal json; but this had the unintended side-effect of breaking existing templates that may now encounter a nil pointer, particularly when accessing member structs. Introduced here, the Expand() methods on Node and Profile are now responsible for populating such fields, and Node.Expand() is now called during template processing. Signed-off-by: Jonathon Anderson <janderson@ciq.com>
406 lines
9.8 KiB
Go
406 lines
9.8 KiB
Go
package node
|
|
|
|
import (
|
|
"net"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/warewulf/warewulf/internal/pkg/util"
|
|
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
|
)
|
|
|
|
type nodeList []Node
|
|
|
|
func (a nodeList) Len() int { return len(a) }
|
|
func (a nodeList) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a nodeList) Less(i, j int) bool { return a[i].id < a[j].id }
|
|
|
|
type profileList []Profile
|
|
|
|
func (a profileList) Len() int { return len(a) }
|
|
func (a profileList) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a profileList) Less(i, j int) bool { return a[i].id < a[j].id }
|
|
|
|
/**********
|
|
*
|
|
* Filters
|
|
*
|
|
*********/
|
|
|
|
/*
|
|
Filter a given slice of NodeConf against a given
|
|
regular expression
|
|
*/
|
|
func FilterNodeListByName(set []Node, searchList []string) []Node {
|
|
var ret []Node
|
|
unique := make(map[string]Node)
|
|
|
|
if len(searchList) > 0 {
|
|
for _, search := range searchList {
|
|
for _, entry := range set {
|
|
if match, _ := regexp.MatchString("^"+search+"$", entry.id); match {
|
|
unique[entry.id] = entry
|
|
}
|
|
}
|
|
}
|
|
for _, n := range unique {
|
|
ret = append(ret, n)
|
|
}
|
|
} else {
|
|
ret = set
|
|
}
|
|
|
|
sort.Sort(nodeList(ret))
|
|
return ret
|
|
}
|
|
|
|
/*
|
|
Filter a given slice of ProfileConf against a given
|
|
regular expression
|
|
*/
|
|
func FilterProfileListByName(set []Profile, searchList []string) []Profile {
|
|
var ret []Profile
|
|
unique := make(map[string]Profile)
|
|
|
|
if len(searchList) > 0 {
|
|
for _, search := range searchList {
|
|
for _, entry := range set {
|
|
if match, _ := regexp.MatchString("^"+search+"$", entry.id); match {
|
|
unique[entry.id] = entry
|
|
}
|
|
}
|
|
}
|
|
for _, n := range unique {
|
|
ret = append(ret, n)
|
|
}
|
|
} else {
|
|
ret = set
|
|
}
|
|
|
|
sort.Sort(profileList(ret))
|
|
return ret
|
|
}
|
|
|
|
/*
|
|
Creates an NodeConf with the given id. Doesn't add it to the database
|
|
*/
|
|
func NewNode(id string) (nodeconf Node) {
|
|
nodeconf = EmptyNode()
|
|
nodeconf.id = id
|
|
return nodeconf
|
|
}
|
|
|
|
func NewProfile(id string) (profileconf Profile) {
|
|
profileconf = EmptyProfile()
|
|
profileconf.id = id
|
|
return profileconf
|
|
}
|
|
|
|
func EmptyNode() (nodeconf Node) {
|
|
nodeconf.Expand()
|
|
return nodeconf
|
|
}
|
|
|
|
/*
|
|
Creates a ProfileConf but doesn't add it to the database.
|
|
*/
|
|
func EmptyProfile() (profileconf Profile) {
|
|
profileconf.Expand()
|
|
return profileconf
|
|
}
|
|
|
|
func (nodeconf *Node) Expand() {
|
|
nodeconf.Profile.Expand()
|
|
}
|
|
|
|
func (profile *Profile) Expand() {
|
|
if profile.Ipmi == nil {
|
|
profile.Ipmi = new(IpmiConf)
|
|
}
|
|
if profile.Ipmi.Tags == nil {
|
|
profile.Ipmi.Tags = make(map[string]string)
|
|
}
|
|
if profile.Kernel == nil {
|
|
profile.Kernel = new(KernelConf)
|
|
}
|
|
if profile.NetDevs == nil {
|
|
profile.NetDevs = make(map[string]*NetDev)
|
|
}
|
|
for i := range profile.NetDevs {
|
|
if profile.NetDevs[i].Tags == nil {
|
|
profile.NetDevs[i].Tags = make(map[string]string)
|
|
}
|
|
}
|
|
if profile.Tags == nil {
|
|
profile.Tags = make(map[string]string)
|
|
}
|
|
}
|
|
|
|
/*
|
|
Flattens out a NodeConf, which means if there are no explicit values in *IpmiConf
|
|
or *KernelConf, these pointer will set to nil. This will remove something like
|
|
ipmi: {} from nodes.conf
|
|
*/
|
|
func (info *Node) Flatten() {
|
|
recursiveFlatten(info)
|
|
}
|
|
|
|
/*
|
|
Flattens out a ProfileConf, which means if there are no explicit values in *IpmiConf
|
|
or *KernelConf, these pointer will set to nil. This will remove something like
|
|
ipmi: {} from nodes.conf
|
|
*/
|
|
func (info *Profile) Flatten() {
|
|
recursiveFlatten(info)
|
|
}
|
|
|
|
func recursiveFlatten(obj interface{}) (hasContent bool) {
|
|
valObj := reflect.ValueOf(obj)
|
|
typeObj := reflect.TypeOf(obj)
|
|
hasContent = false
|
|
if valObj.IsNil() {
|
|
return
|
|
}
|
|
for i := 0; i < typeObj.Elem().NumField(); i++ {
|
|
if valObj.Elem().Field(i).IsValid() {
|
|
if !typeObj.Elem().Field(i).IsExported() {
|
|
continue
|
|
}
|
|
}
|
|
switch typeObj.Elem().Field(i).Type.Kind() {
|
|
case reflect.Map:
|
|
mapIter := valObj.Elem().Field(i).MapRange()
|
|
for mapIter.Next() {
|
|
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
|
|
}
|
|
default:
|
|
hasContent = !mapIter.Value().IsZero() || hasContent
|
|
}
|
|
}
|
|
|
|
case reflect.Ptr:
|
|
if valObj.Elem().Field(i).Addr().IsValid() {
|
|
ret := recursiveFlatten((valObj.Elem().Field(i).Interface()))
|
|
if !ret {
|
|
valObj.Elem().Field(i).Set(reflect.Zero(valObj.Elem().Field(i).Type()))
|
|
}
|
|
hasContent = ret || hasContent
|
|
|
|
}
|
|
case reflect.Struct:
|
|
ret := recursiveFlatten((valObj.Elem().Field(i).Addr().Interface()))
|
|
hasContent = ret || hasContent
|
|
case reflect.Slice:
|
|
if typeObj.Elem().Field(i).Type == reflect.TypeOf([]string{}) {
|
|
del := false
|
|
for _, elem := range (valObj.Elem().Field(i).Interface()).([]string) {
|
|
if strings.EqualFold(elem, undef) {
|
|
del = true
|
|
}
|
|
}
|
|
if del {
|
|
valObj.Elem().Field(i).SetLen(0)
|
|
}
|
|
}
|
|
if valObj.Elem().Field(i).Len() > 0 {
|
|
hasContent = true
|
|
}
|
|
case reflect.String:
|
|
if strings.EqualFold(valObj.Elem().Field(i).String(), undef) {
|
|
valObj.Elem().Field(i).SetString("")
|
|
}
|
|
if valObj.Elem().Field(i).String() != "" {
|
|
hasContent = true
|
|
}
|
|
case reflect.Bool:
|
|
val := valObj.Elem().Field(i).Interface().(bool)
|
|
hasContent = hasContent || val
|
|
default:
|
|
switch valObj.Elem().Field(i).Type() {
|
|
case reflect.TypeOf(net.IP{}):
|
|
val := valObj.Elem().Field(i).Interface().(net.IP)
|
|
if len(val) != 0 && !val.IsUnspecified() {
|
|
hasContent = true
|
|
}
|
|
default:
|
|
}
|
|
}
|
|
if !hasContent {
|
|
valObj.Elem().Field(i).Set(reflect.Zero(valObj.Elem().Field(i).Type()))
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
/*
|
|
Create a string slice, where every element represents a yaml entry, used for node/profile edit
|
|
in order to get a summary of all available elements
|
|
*/
|
|
func UnmarshalConf(obj interface{}, excludeList []string) (lines []string) {
|
|
objType := reflect.TypeOf(obj)
|
|
// now iterate of every field
|
|
for i := 0; i < objType.NumField(); i++ {
|
|
field := objType.Field(i)
|
|
if field.Tag.Get("comment") != "" {
|
|
if ymlStr, ok := getYamlString(field, excludeList); ok {
|
|
lines = append(lines, ymlStr...)
|
|
}
|
|
}
|
|
if field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct && field.Tag.Get("yaml") != "" {
|
|
typeLine := field.Tag.Get("yaml")
|
|
if len(strings.Split(typeLine, ",")) > 1 {
|
|
typeLine = strings.Split(typeLine, ",")[0] + ":"
|
|
}
|
|
lines = append(lines, typeLine)
|
|
nestedLine := UnmarshalConf(reflect.New(field.Type.Elem()).Elem().Interface(), excludeList)
|
|
for _, ln := range nestedLine {
|
|
lines = append(lines, " "+ln)
|
|
}
|
|
} else if field.Type.Kind() == reflect.Map && field.Type.Elem().Kind() == reflect.Ptr {
|
|
typeLine := field.Tag.Get("yaml")
|
|
if len(strings.Split(typeLine, ",")) > 1 {
|
|
typeLine = strings.Split(typeLine, ",")[0] + ":"
|
|
}
|
|
lines = append(lines, typeLine, " element:")
|
|
nestedLine := UnmarshalConf(reflect.New(field.Type.Elem().Elem()).Elem().Interface(), excludeList)
|
|
for _, ln := range nestedLine {
|
|
lines = append(lines, " "+ln)
|
|
}
|
|
} else if field.Type.Kind() == reflect.Struct && field.Anonymous {
|
|
nestedLine := UnmarshalConf(reflect.New(field.Type).Elem().Interface(), excludeList)
|
|
lines = append(lines, nestedLine...)
|
|
}
|
|
}
|
|
return lines
|
|
}
|
|
|
|
/*
|
|
Get the string of the yaml tag
|
|
*/
|
|
func getYamlString(myType reflect.StructField, excludeList []string) ([]string, bool) {
|
|
ymlStr := myType.Tag.Get("yaml")
|
|
if len(strings.Split(ymlStr, ",")) > 1 {
|
|
ymlStr = strings.Split(ymlStr, ",")[0]
|
|
}
|
|
if util.InSlice(excludeList, ymlStr) {
|
|
return []string{""}, false
|
|
} else if myType.Tag.Get("comment") == "" && myType.Type.Kind() == reflect.String {
|
|
return []string{""}, false
|
|
}
|
|
if myType.Type.Kind() == reflect.String {
|
|
fieldType := myType.Tag.Get("type")
|
|
if fieldType == "" {
|
|
fieldType = "string"
|
|
}
|
|
ymlStr += ": " + fieldType
|
|
return []string{ymlStr}, true
|
|
} else if myType.Type == reflect.TypeOf([]string{}) {
|
|
return []string{ymlStr + ":", " - string"}, true
|
|
} else if myType.Type == reflect.TypeOf(map[string]string{}) {
|
|
return []string{ymlStr + ":", " key: value"}, true
|
|
} else if myType.Type.Kind() == reflect.Ptr {
|
|
return []string{ymlStr + ":"}, true
|
|
}
|
|
return []string{ymlStr}, true
|
|
}
|
|
|
|
/*
|
|
Getters for unexported fields
|
|
*/
|
|
|
|
/*
|
|
Returns the id of the node
|
|
*/
|
|
func (node *Node) Id() string {
|
|
return node.id
|
|
}
|
|
|
|
/*
|
|
Returns the id of the profile
|
|
*/
|
|
func (node *Profile) Id() string {
|
|
return node.id
|
|
}
|
|
|
|
/*
|
|
Returns if the node is a valid in the database
|
|
*/
|
|
func (node *Node) Valid() bool {
|
|
return node.valid
|
|
}
|
|
|
|
func (node *Node) updatePrimaryNetDev() {
|
|
if netdev, ok := node.NetDevs[node.PrimaryNetDev]; ok {
|
|
netdev.primary = true
|
|
} else {
|
|
keys := make([]string, 0)
|
|
for k := range node.NetDevs {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
if len(keys) > 0 {
|
|
wwlog.Debug("%s: no primary defined, sanitizing to: %s", node.id, keys[0])
|
|
node.NetDevs[keys[0]].primary = true
|
|
node.PrimaryNetDev = keys[0]
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Check if the netdev is the primary one
|
|
*/
|
|
func (dev *NetDev) Primary() bool {
|
|
return dev.primary
|
|
}
|
|
|
|
// returns all negated elements which are marked with ! as prefix
|
|
// from a list
|
|
func negList(list []string) (ret []string) {
|
|
for _, tok := range list {
|
|
if strings.HasPrefix(tok, "~") {
|
|
ret = append(ret, tok[1:])
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// clean a list from negated tokens
|
|
func cleanList(list []string) (ret []string) {
|
|
neg := negList(list)
|
|
for _, listTok := range list {
|
|
notNegate := true
|
|
for _, negTok := range neg {
|
|
if listTok == negTok || listTok == "~"+negTok {
|
|
notNegate = false
|
|
}
|
|
}
|
|
if notNegate {
|
|
ret = append(ret, listTok)
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
/*
|
|
Return the ipv4 address and mask in CIDR format. Aimed for the use in
|
|
templates.
|
|
*/
|
|
func (netdev *NetDev) IpCIDR() string {
|
|
if netdev.Ipaddr.IsUnspecified() || netdev.Netmask.IsUnspecified() {
|
|
return ""
|
|
}
|
|
ipCIDR := net.IPNet{
|
|
IP: netdev.Ipaddr,
|
|
Mask: net.IPMask(netdev.Netmask),
|
|
}
|
|
return ipCIDR.String()
|
|
}
|