Refactor gRPC-based node functions

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Jonathon Anderson
2026-03-21 21:15:12 -06:00
parent 3c895dd941
commit ec6884a878
15 changed files with 430 additions and 694 deletions

View File

@@ -2,14 +2,14 @@ package add
import (
"fmt"
"net"
"strings"
"gopkg.in/yaml.v3"
"github.com/spf13/cobra"
apinode "github.com/warewulf/warewulf/internal/pkg/api/node"
"github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1"
"github.com/warewulf/warewulf/internal/pkg/hostlist"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/util"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
@@ -60,16 +60,45 @@ func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) error {
}
}
}
buffer, err := yaml.Marshal(vars.nodeConf)
nodeDB, err := node.New()
if err != nil {
wwlog.Error("Can't marshall nodeInfo", err)
return err
return fmt.Errorf("failed to open node database: %w", err)
}
set := wwapiv1.NodeAddParameter{
NodeConfYaml: string(buffer[:]),
NodeNames: args,
Force: true,
nodeArgs := hostlist.Expand(args)
changed := cmd.Flags().Changed
var ipv4, ipmiaddr net.IP
for _, a := range nodeArgs {
n, err := nodeDB.AddNode(a)
if err != nil {
return fmt.Errorf("failed to add node: %w", err)
}
n.UpdateFrom(&vars.nodeConf, changed)
if !changed("profile") && len(vars.nodeConf.Profiles) > 0 {
n.Profiles = vars.nodeConf.Profiles
}
wwlog.Info("Added node: %s", a)
for _, dev := range n.NetDevs {
if !ipv4.IsUnspecified() && ipv4 != nil {
ipv4 = util.IncrementIPv4(ipv4, 1)
wwlog.Verbose("Incremented IP addr to %s", ipv4)
dev.Ipaddr = ipv4
} else if !dev.Ipaddr.IsUnspecified() {
ipv4 = dev.Ipaddr
}
}
if n.Ipmi != nil {
if !ipmiaddr.IsUnspecified() && ipmiaddr != nil {
ipmiaddr = util.IncrementIPv4(ipmiaddr, 1)
wwlog.Verbose("Incremented ipmi IP addr to %s", ipmiaddr)
n.Ipmi.Ipaddr = ipmiaddr
} else if !n.Ipmi.Ipaddr.IsUnspecified() {
ipmiaddr = n.Ipmi.Ipaddr
}
}
}
return apinode.NodeAdd(&set)
if err := nodeDB.Persist(); err != nil {
return fmt.Errorf("failed to persist new node: %w", err)
}
return warewulfd.DaemonReload()
}
}

View File

@@ -2,36 +2,64 @@ package delete
import (
"fmt"
"os"
"github.com/spf13/cobra"
apiNode "github.com/warewulf/warewulf/internal/pkg/api/node"
"github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1"
"github.com/warewulf/warewulf/internal/pkg/hostlist"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/util"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
func CobraRunE(cmd *cobra.Command, args []string) (err error) {
nodeDB, err := node.New()
if err != nil {
return fmt.Errorf("failed to open node database: %w", err)
}
ndp := wwapiv1.NodeDeleteParameter{
Force: SetForce,
NodeNames: args,
nodes, err := nodeDB.FindAllNodes()
if err != nil {
return fmt.Errorf("could not get node list: %w", err)
}
nodeArgs := hostlist.Expand(args)
var nodeList []node.Node
for _, r := range nodeArgs {
var match bool
for _, n := range nodes {
if n.Id() == r {
nodeList = append(nodeList, n)
match = true
}
}
if !match {
fmt.Fprintf(os.Stderr, "ERROR: No match for node: %s\n", r)
}
}
if len(nodeList) == 0 {
fmt.Printf("No nodes found\n")
return
}
if !SetYes {
var nodeList []node.Node
// The checks run twice in the prompt case.
// Avoiding putting in a blocking prompt in an API.
nodeList, err = apiNode.NodeDeleteParameterCheck(&ndp, false)
if err != nil {
return
}
if len(nodeList) == 0 {
return
}
yes := util.Confirm(fmt.Sprintf("Are you sure you want to delete %d nodes(s)", len(nodeList)))
if !yes {
return
}
}
return apiNode.NodeDelete(&ndp)
for _, n := range nodeList {
if err := nodeDB.DelNode(n.Id()); err != nil {
wwlog.Error("%s", err)
} else {
wwlog.Verbose("Deleting node: %s\n", n.Id())
}
}
if err := nodeDB.Persist(); err != nil {
return fmt.Errorf("failed to persist nodedb: %w", err)
}
return warewulfd.DaemonReload()
}

View File

@@ -6,8 +6,6 @@ import (
"os"
"github.com/spf13/cobra"
apinode "github.com/warewulf/warewulf/internal/pkg/api/node"
"github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/util"
"gopkg.in/yaml.v3"
@@ -32,9 +30,22 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
}
if setYes || util.Confirm(fmt.Sprintf("Are you sure you want to modify %d nodes", len(importMap))) {
err = apinode.NodeAddFromYaml(&wwapiv1.NodeYaml{NodeConfMapYaml: string(buffer)})
nodeDB, err := node.New()
if err != nil {
return fmt.Errorf("got following problem when writing back yaml: %s", err)
return fmt.Errorf("could not open NodeDB: %w", err)
}
for nodeName, nodeData := range importMap {
if _, err := nodeDB.GetNodeOnly(nodeName); err == node.ErrNotFound {
if _, err := nodeDB.AddNode(nodeName); err != nil {
return fmt.Errorf("couldn't add new node: %w", err)
}
}
if err := nodeDB.SetNode(nodeName, *nodeData); err != nil {
return fmt.Errorf("couldn't set node: %w", err)
}
}
if err := nodeDB.Persist(); err != nil {
return fmt.Errorf("failed to persist nodedb: %w", err)
}
}

View File

@@ -1,48 +1,117 @@
package list
import (
"encoding/json"
"sort"
"strings"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/warewulf/warewulf/internal/app/wwctl/table"
apinode "github.com/warewulf/warewulf/internal/pkg/api/node"
"github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1"
"github.com/warewulf/warewulf/internal/pkg/hostlist"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) {
return func(cmd *cobra.Command, args []string) (err error) {
req := wwapiv1.GetNodeList{
Nodes: args,
Type: wwapiv1.GetNodeList_Simple,
nodeDB, err := node.New()
if err != nil {
return
}
if vars.showAll {
req.Type = wwapiv1.GetNodeList_All
} else if vars.showIpmi {
req.Type = wwapiv1.GetNodeList_Ipmi
} else if vars.showNet {
req.Type = wwapiv1.GetNodeList_Network
} else if vars.showLong {
req.Type = wwapiv1.GetNodeList_Long
} else if vars.showYaml {
req.Type = wwapiv1.GetNodeList_YAML
} else if vars.showJson {
req.Type = wwapiv1.GetNodeList_JSON
nodes, err := nodeDB.FindAllNodes()
if err != nil {
return
}
nodeInfo, err := apinode.NodeList(&req)
nodeNames := hostlist.Expand(args)
sort.Strings(nodeNames)
filtered := node.FilterNodeListByName(nodes, nodeNames)
if len(nodeInfo.Output) > 0 {
if req.Type == wwapiv1.GetNodeList_YAML || req.Type == wwapiv1.GetNodeList_JSON {
wwlog.Info(nodeInfo.Output[0])
} else {
t := table.New(cmd.OutOrStdout())
t.AddHeader(table.Prep(strings.Split(nodeInfo.Output[0], ":=:"))...)
for _, val := range nodeInfo.Output[1:] {
t.AddLine(table.Prep(strings.Split(val, ":=:"))...)
}
t.Print()
if vars.showYaml || vars.showJson {
nodeMap := make(map[string]node.Node)
for _, n := range filtered {
nodeMap[n.Id()] = n
}
var buf []byte
if vars.showJson {
buf, _ = json.MarshalIndent(nodeMap, "", " ")
} else {
buf, _ = yaml.Marshal(nodeMap)
}
wwlog.Info(string(buf))
} else if vars.showAll {
t := table.New(cmd.OutOrStdout())
t.AddHeader("NODE", "FIELD", "PROFILE", "VALUE")
for _, n := range filtered {
if _, fields, err := nodeDB.MergeNode(n.Id()); err != nil {
wwlog.Error("unable to merge node %v: %v", n.Id(), err)
continue
} else {
for _, f := range fields.List(n) {
t.AddLine(table.Prep([]string{n.Id(), f.Field, f.Source, f.Value})...)
}
}
}
t.Print()
} else if vars.showIpmi {
t := table.New(cmd.OutOrStdout())
t.AddHeader("NODE", "IPMI IPADDR", "IPMI PORT", "IPMI USERNAME", "IPMI INTERFACE")
for _, n := range filtered {
ipaddr, port, username, iface := "", "", "", ""
if n.Ipmi != nil {
ipaddr = n.Ipmi.Ipaddr.String()
port = n.Ipmi.Port
username = n.Ipmi.UserName
iface = n.Ipmi.Interface
}
t.AddLine(table.Prep([]string{n.Id(), ipaddr, port, username, iface})...)
}
t.Print()
} else if vars.showNet {
t := table.New(cmd.OutOrStdout())
t.AddHeader("NODE", "NETWORK", "HWADDR", "IPADDR", "GATEWAY", "DEVICE")
for _, n := range filtered {
if len(n.NetDevs) > 0 {
for name := range n.NetDevs {
t.AddLine(table.Prep([]string{n.Id(), name,
n.NetDevs[name].Hwaddr,
n.NetDevs[name].Ipaddr.String(),
n.NetDevs[name].Gateway.String(),
n.NetDevs[name].Device})...)
}
} else {
t.AddLine(table.Prep([]string{n.Id(), "", "", "", "", ""})...)
}
}
t.Print()
} else if vars.showLong {
t := table.New(cmd.OutOrStdout())
t.AddHeader("NODE NAME", "KERNEL VERSION", "IMAGE", "OVERLAYS (S/R)")
for _, n := range filtered {
kernelVersion := ""
if n.Kernel != nil {
kernelVersion = n.Kernel.Version
}
t.AddLine(table.Prep([]string{n.Id(),
kernelVersion,
n.ImageName,
strings.Join(n.SystemOverlay, ",") + "/" + strings.Join(n.RuntimeOverlay, ",")})...)
}
t.Print()
} else {
// Simple (default)
t := table.New(cmd.OutOrStdout())
t.AddHeader("NODE NAME", "PROFILES", "NETWORK")
for _, n := range filtered {
var netNames []string
for k := range n.NetDevs {
netNames = append(netNames, k)
}
sort.Strings(netNames)
t.AddLine(table.Prep([]string{n.Id(), strings.Join(n.Profiles, ","), strings.Join(netNames, ", ")})...)
}
t.Print()
}
return
}

View File

@@ -5,13 +5,11 @@ import (
"strings"
"github.com/spf13/cobra"
apinode "github.com/warewulf/warewulf/internal/pkg/api/node"
"github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1"
"github.com/warewulf/warewulf/internal/pkg/hostlist"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/util"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
"gopkg.in/yaml.v3"
)
func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) {
@@ -48,44 +46,108 @@ func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err err
}
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)
}
wwlog.Debug("sending following values: %s", string(buffer))
args = hostlist.Expand(args)
set := wwapiv1.ConfSetParameter{
NodeConfYaml: string(buffer),
NetdevDelete: vars.nodeDel.NetDel,
PartitionDelete: vars.nodeDel.PartDel,
DiskDelete: vars.nodeDel.DiskDel,
FilesystemDelete: vars.nodeDel.FsDel,
TagAdd: vars.nodeAdd.TagsAdd,
TagDel: vars.nodeDel.TagsDel,
NetTagAdd: vars.nodeAdd.NetTagsAdd,
NetTagDel: vars.nodeDel.NetTagsDel,
IpmiTagAdd: vars.nodeAdd.IpmiTagsAdd,
IpmiTagDel: vars.nodeDel.IpmiTagsDel,
Netdev: vars.nodeAdd.Net,
AllConfs: vars.setNodeAll,
Force: vars.setForce,
ConfList: args,
nodeDB, err := node.New()
if err != nil {
return fmt.Errorf("could not open configuration: %w", err)
}
args = hostlist.Expand(args)
if len(args) == 0 && !vars.setNodeAll {
return fmt.Errorf("no nodes specified; use --all to modify all nodes")
}
if vars.setNodeAll {
args = nodeDB.ListAllNodes()
wwlog.Warn("this command will modify all nodes")
} else if len(nodeDB.ListAllNodes()) == 0 {
wwlog.Warn("no nodes/profiles found")
return nil
}
changed := cmd.Flags().Changed
var count uint
for _, nId := range args {
wwlog.Debug("evaluating node: %s", nId)
nodePtr, err := nodeDB.GetNodeOnlyPtr(nId)
if err != nil {
wwlog.Warn("invalid node: %s", nId)
continue
}
nodePtr.UpdateFrom(&vars.nodeConf, changed)
if vars.nodeDel.NetDel != "" {
if _, ok := nodePtr.NetDevs[vars.nodeDel.NetDel]; !ok {
return fmt.Errorf("network device name doesn't exist: %s", vars.nodeDel.NetDel)
}
wwlog.Verbose("Node: %s, Deleting network device: %s", nId, vars.nodeDel.NetDel)
delete(nodePtr.NetDevs, vars.nodeDel.NetDel)
}
if vars.nodeDel.PartDel != "" {
for diskname, disk := range nodePtr.Disks {
if _, ok := disk.Partitions[vars.nodeDel.PartDel]; ok {
wwlog.Verbose("Node: %s, on disk %s, deleting partition: %s", nId, diskname, vars.nodeDel.PartDel)
delete(disk.Partitions, vars.nodeDel.PartDel)
} else {
return fmt.Errorf("partition doesn't exist: %s", vars.nodeDel.PartDel)
}
}
}
if vars.nodeDel.DiskDel != "" {
if _, ok := nodePtr.Disks[vars.nodeDel.DiskDel]; ok {
wwlog.Verbose("Node: %s, deleting disk: %s", nId, vars.nodeDel.DiskDel)
delete(nodePtr.Disks, vars.nodeDel.DiskDel)
} else {
return fmt.Errorf("disk doesn't exist: %s", vars.nodeDel.DiskDel)
}
}
if vars.nodeDel.FsDel != "" {
if _, ok := nodePtr.FileSystems[vars.nodeDel.FsDel]; ok {
wwlog.Verbose("Node: %s, deleting filesystem: %s", nId, vars.nodeDel.FsDel)
delete(nodePtr.FileSystems, vars.nodeDel.FsDel)
} else {
return fmt.Errorf("filesystem doesn't exist: %s", vars.nodeDel.FsDel)
}
}
for _, key := range vars.nodeDel.TagsDel {
delete(nodePtr.Tags, key)
}
for key, val := range vars.nodeAdd.TagsAdd {
if nodePtr.Tags == nil {
nodePtr.Tags = make(map[string]string)
}
nodePtr.Tags[key] = val
}
for key, val := range vars.nodeAdd.IpmiTagsAdd {
if nodePtr.Ipmi.Tags == nil {
nodePtr.Ipmi.Tags = make(map[string]string)
}
nodePtr.Ipmi.Tags[key] = val
}
for _, key := range vars.nodeDel.IpmiTagsDel {
delete(nodePtr.Ipmi.Tags, key)
}
if netDev, ok := nodePtr.NetDevs[vars.nodeAdd.Net]; ok {
for _, key := range vars.nodeDel.NetTagsDel {
delete(netDev.Tags, key)
}
if len(vars.nodeAdd.NetTagsAdd) > 0 && netDev.Tags == nil {
netDev.Tags = make(map[string]string)
}
for key, val := range vars.nodeAdd.NetTagsAdd {
netDev.Tags[key] = val
}
}
count++
}
if !vars.setYes {
var nodeCount uint
// The checks run twice in the prompt case.
// Avoiding putting in a blocking prompt in an API.
_, nodeCount, err = apinode.NodeSetParameterCheck(&set)
if err != nil {
return nil
}
yes := util.Confirm(fmt.Sprintf("Are you sure you want to modify %d nodes(s)", nodeCount))
if !yes {
if !util.Confirm(fmt.Sprintf("Are you sure you want to modify %d nodes(s)", count)) {
return nil
}
}
return apinode.NodeSet(&set)
if err := nodeDB.Persist(); err != nil {
return err
}
return warewulfd.DaemonReload()
}
}

View File

@@ -681,6 +681,22 @@ nodes:
number: "1"
wipe_partition_entry: true`,
},
"--all modifies all nodes": {
args: []string{"--all", "--comment=batch-update"},
wantErr: false,
inDB: `
nodeprofiles: {}
nodes:
n01: {}
n02: {}`,
outDB: `
nodeprofiles: {}
nodes:
n01:
comment: batch-update
n02:
comment: batch-update`,
},
}
for name, tt := range tests {

View File

@@ -26,7 +26,7 @@ func GetCommand() *cobra.Command {
Short: "Configure node properties",
Long: "This command sets configuration properties for nodes matching PATTERN.\n\nNote: use the string 'UNSET' to remove a configuration\n" + hostlist.Docstring,
Aliases: []string{"modify"},
Args: cobra.MinimumNArgs(1), // require pattern as a mandatory arg
Args: cobra.ArbitraryArgs,
RunE: CobraRunE(&vars),
ValidArgsFunction: completions.Nodes,
}

View File

@@ -1,21 +1,29 @@
package nodestatus
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
"time"
"github.com/fatih/color"
"github.com/spf13/cobra"
apinode "github.com/warewulf/warewulf/internal/pkg/api/node"
"github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1"
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
"github.com/warewulf/warewulf/internal/pkg/hostlist"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
"golang.org/x/term"
)
type nodeStatus struct {
NodeName string `json:"node name"`
Stage string `json:"stage"`
Sent string `json:"sent"`
Ipaddr string `json:"ipaddr"`
Lastseen int64 `json:"last seen"`
}
func displayStage(stage string) string {
switch stage {
case "efiboot":
@@ -54,10 +62,21 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) {
var count int
rightnow := time.Now().Unix()
var nodeStatusResponse *wwapiv1.NodeStatusResponse
nodeStatusResponse, err = apinode.NodeStatus([]string{})
statusURL := fmt.Sprintf("http://%s:%d/status", controller.Ipaddr, controller.Warewulf.Port)
wwlog.Verbose("Connecting to: %s", statusURL)
resp, err := http.Get(statusURL)
if err != nil {
return err
return fmt.Errorf("could not connect to Warewulf server: %w", err)
}
var wwNodeStatus struct {
Nodes map[string]*nodeStatus `json:"nodes"`
}
err = json.NewDecoder(resp.Body).Decode(&wwNodeStatus)
_ = resp.Body.Close()
if err != nil {
return fmt.Errorf("could not decode JSON: %w", err)
}
if SetWatch {
@@ -73,20 +92,20 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) {
fmt.Printf("%s\n", strings.Repeat("=", 80))
wwlog.Verbose("Building sort index")
var statuses []*wwapiv1.NodeStatus
var statuses []*nodeStatus
if len(args) > 0 {
nodeList := hostlist.Expand(args)
for i := 0; i < len(nodeStatusResponse.NodeStatus); i++ {
for j := 0; j < len(nodeList); j++ {
if nodeStatusResponse.NodeStatus[i].NodeName == nodeList[j] {
statuses = append(statuses, nodeStatusResponse.NodeStatus[i])
for _, v := range wwNodeStatus.Nodes {
for _, name := range nodeList {
if v.NodeName == name {
statuses = append(statuses, v)
break
}
}
}
} else {
for i := 0; i < len(nodeStatusResponse.NodeStatus); i++ {
statuses = append(statuses, nodeStatusResponse.NodeStatus[i])
for _, v := range wwNodeStatus.Nodes {
statuses = append(statuses, v)
}
}