Files
warewulf/internal/pkg/node/methods.go
2025-03-25 18:16:14 -06:00

430 lines
10 KiB
Go

package node
import (
"net"
"reflect"
"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 entry.id == search {
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 entry.id == search {
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 isUnsetValue(elem) {
del = true
}
}
if del {
valObj.Elem().Field(i).SetLen(0)
}
}
if valObj.Elem().Field(i).Len() > 0 {
hasContent = true
}
case reflect.String:
if isUnsetValue(valObj.Elem().Field(i).String()) {
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
}
// ContainerName returns the value of the ImageName field for backwards-compatibility.
func (node *Node) ContainerName() string {
return node.ImageName
}
// ContainerName returns the value of the ImageName field for backwards-compatibility.
func (profile *Profile) ContainerName() string {
return profile.ImageName
}
/*
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
}
func (p *Profile) cleanLists() {
p.Profiles = cleanList(p.Profiles)
p.SystemOverlay = cleanList(p.SystemOverlay)
p.RuntimeOverlay = cleanList(p.RuntimeOverlay)
if p.Kernel != nil {
p.Kernel.Args = cleanList(p.Kernel.Args)
}
}
// 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 == nil || netdev.Ipaddr.IsUnspecified() || netdev.Netmask == nil || netdev.Netmask.IsUnspecified() {
return ""
}
ipCIDR := net.IPNet{
IP: netdev.Ipaddr,
Mask: net.IPMask(netdev.Netmask),
}
return ipCIDR.String()
}
var unsetValues = []string{"UNSET", "UNDEF"}
func isUnsetValue(value string) bool {
return util.InSlice(unsetValues, value)
}