Skip to content

Commit

Permalink
cli: show host_network in nomad status (#11432)
Browse files Browse the repository at this point in the history
Enhance the CLI in order to return the host network in two flavors 
(default, verbose) of the `node status` command.

Fixes: #11223.
Signed-off-by: Alessandro De Blasis <alex@deblasis.net>
  • Loading branch information
deblasis committed Nov 5, 2021
1 parent 5ad8bb0 commit 9a5248b
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/11432.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
cli: the command `node status` now returns `host_network` information as well
```
9 changes: 9 additions & 0 deletions api/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,14 @@ type HostVolumeInfo struct {
ReadOnly bool
}

//HostNetworkInfo is used to return metadata about a given HostNetwork
type HostNetworkInfo struct {
Name string
CIDR string
Interface string
ReservedPorts string
}

type DrainStatus string

// DrainMetadata contains information about the most recent drain operation for a given Node.
Expand Down Expand Up @@ -541,6 +549,7 @@ type Node struct {
Events []*NodeEvent
Drivers map[string]*DriverInfo
HostVolumes map[string]*HostVolumeInfo
HostNetworks map[string]*HostNetworkInfo
CSIControllerPlugins map[string]*CSIInfo
CSINodePlugins map[string]*CSIInfo
LastDrain *DrainMetadata
Expand Down
8 changes: 8 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1426,6 +1426,14 @@ func (c *Client) setupNode() error {
}
}
}
if node.HostNetworks == nil {
if l := len(c.config.HostNetworks); l != 0 {
node.HostNetworks = make(map[string]*structs.ClientHostNetworkConfig, l)
for k, v := range c.config.HostNetworks {
node.HostNetworks[k] = v.Copy()
}
}
}

if node.Name == "" {
node.Name = node.ID
Expand Down
34 changes: 34 additions & 0 deletions command/node_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,16 @@ func nodeVolumeNames(n *api.Node) []string {
return volumes
}

func nodeNetworkNames(n *api.Node) []string {
var networks []string
for name := range n.HostNetworks {
networks = append(networks, name)
}

sort.Strings(networks)
return networks
}

func formatDrain(n *api.Node) string {
if n.DrainStrategy != nil {
b := new(strings.Builder)
Expand Down Expand Up @@ -400,6 +410,7 @@ func (c *NodeStatusCommand) formatNode(client *api.Client, node *api.Node) int {

if c.short {
basic = append(basic, fmt.Sprintf("Host Volumes|%s", strings.Join(nodeVolumeNames(node), ",")))
basic = append(basic, fmt.Sprintf("Host Networks|%s", strings.Join(nodeNetworkNames(node), ",")))
basic = append(basic, fmt.Sprintf("CSI Volumes|%s", strings.Join(nodeCSIVolumeNames(node, runningAllocs), ",")))
basic = append(basic, fmt.Sprintf("Drivers|%s", strings.Join(nodeDrivers(node), ",")))
c.Ui.Output(c.Colorize().Color(formatKV(basic)))
Expand Down Expand Up @@ -428,6 +439,7 @@ func (c *NodeStatusCommand) formatNode(client *api.Client, node *api.Node) int {
// driver info in the basic output
if !c.verbose {
basic = append(basic, fmt.Sprintf("Host Volumes|%s", strings.Join(nodeVolumeNames(node), ",")))
basic = append(basic, fmt.Sprintf("Host Networks|%s", strings.Join(nodeNetworkNames(node), ",")))
basic = append(basic, fmt.Sprintf("CSI Volumes|%s", strings.Join(nodeCSIVolumeNames(node, runningAllocs), ",")))
driverStatus := fmt.Sprintf("Driver Status| %s", c.outputTruncatedNodeDriverInfo(node))
basic = append(basic, driverStatus)
Expand All @@ -439,6 +451,7 @@ func (c *NodeStatusCommand) formatNode(client *api.Client, node *api.Node) int {
// If we're running in verbose mode, include full host volume and driver info
if c.verbose {
c.outputNodeVolumeInfo(node)
c.outputNodeNetworkInfo(node)
c.outputNodeCSIVolumeInfo(client, node, runningAllocs)
c.outputNodeDriverInfo(node)
}
Expand Down Expand Up @@ -544,6 +557,27 @@ func (c *NodeStatusCommand) outputNodeVolumeInfo(node *api.Node) {
}
}

func (c *NodeStatusCommand) outputNodeNetworkInfo(node *api.Node) {

names := make([]string, 0, len(node.HostNetworks))
for name := range node.HostNetworks {
names = append(names, name)
}
sort.Strings(names)

output := make([]string, 0, len(names)+1)
output = append(output, "Name|CIDR|Interface|ReservedPorts")

if len(names) > 0 {
c.Ui.Output(c.Colorize().Color("\n[bold]Host Networks"))
for _, hostNetworkName := range names {
info := node.HostNetworks[hostNetworkName]
output = append(output, fmt.Sprintf("%s|%v|%s|%s", hostNetworkName, info.CIDR, info.Interface, info.ReservedPorts))
}
c.Ui.Output(formatList(output))
}
}

func (c *NodeStatusCommand) outputNodeCSIVolumeInfo(client *api.Client, node *api.Node, runningAllocs []*api.Allocation) {

// Duplicate nodeCSIVolumeNames to sort by name but also index volume names to ids
Expand Down
123 changes: 123 additions & 0 deletions command/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package command

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/nomad/command/agent"
Expand All @@ -11,6 +12,7 @@ import (
"github.com/mitchellh/cli"
"github.com/posener/complete"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestStatusCommand_Run_JobStatus(t *testing.T) {
Expand Down Expand Up @@ -233,3 +235,124 @@ func TestStatusCommand_AutocompleteArgs(t *testing.T) {
res := predictor.Predict(args)
assert.Contains(res, job.ID)
}

func TestStatusCommand_Run_HostNetwork(t *testing.T) {
t.Parallel()

ui := cli.NewMockUi()

testCases := []struct {
name string
clientHostNetworks []*structs.ClientHostNetworkConfig
verbose bool
assertions func(string)
}{
{
name: "short",
clientHostNetworks: []*structs.ClientHostNetworkConfig{{
Name: "internal",
CIDR: "127.0.0.1/8",
Interface: "lo",
}},
verbose: false,
assertions: func(out string) {
hostNetworksRegexpStr := `Host Networks\s+=\s+internal\n`
require.Regexp(t, regexp.MustCompile(hostNetworksRegexpStr), out)
},
},
{
name: "verbose",
clientHostNetworks: []*structs.ClientHostNetworkConfig{{
Name: "internal",
CIDR: "127.0.0.1/8",
Interface: "lo",
}},
verbose: true,
assertions: func(out string) {
verboseHostNetworksHeadRegexpStr := `Name\s+CIDR\s+Interface\s+ReservedPorts\n`
require.Regexp(t, regexp.MustCompile(verboseHostNetworksHeadRegexpStr), out)

verboseHostNetworksBodyRegexpStr := `internal\s+127\.0\.0\.1/8\s+lo\s+<none>\n`
require.Regexp(t, regexp.MustCompile(verboseHostNetworksBodyRegexpStr), out)
},
},
{
name: "verbose_nointerface",
clientHostNetworks: []*structs.ClientHostNetworkConfig{{
Name: "public",
CIDR: "10.199.0.200/24",
}},
verbose: true,
assertions: func(out string) {
verboseHostNetworksHeadRegexpStr := `Name\s+CIDR\s+Interface\s+ReservedPorts\n`
require.Regexp(t, regexp.MustCompile(verboseHostNetworksHeadRegexpStr), out)

verboseHostNetworksBodyRegexpStr := `public\s+10\.199\.0\.200/24\s+<none>\s+<none>\n`
require.Regexp(t, regexp.MustCompile(verboseHostNetworksBodyRegexpStr), out)
},
},
{
name: "verbose_nointerface_with_reservedports",
clientHostNetworks: []*structs.ClientHostNetworkConfig{{
Name: "public",
CIDR: "10.199.0.200/24",
ReservedPorts: "8080,8081",
}},
verbose: true,
assertions: func(out string) {
verboseHostNetworksHeadRegexpStr := `Name\s+CIDR\s+Interface\s+ReservedPorts\n`
require.Regexp(t, regexp.MustCompile(verboseHostNetworksHeadRegexpStr), out)

verboseHostNetworksBodyRegexpStr := `public\s+10\.199\.0\.200/24\s+<none>\s+8080,8081\n`
require.Regexp(t, regexp.MustCompile(verboseHostNetworksBodyRegexpStr), out)
},
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {

// Start in dev mode so we get a node registration
srv, client, url := testServer(t, true, func(c *agent.Config) {
c.Client.HostNetworks = tt.clientHostNetworks
})
defer srv.Shutdown()

cmd := &StatusCommand{Meta: Meta{Ui: ui, flagAddress: url}}

// Wait for a node to appear
var nodeID string
testutil.WaitForResult(func() (bool, error) {
nodes, _, err := client.Nodes().List(nil)
if err != nil {
return false, err
}
if len(nodes) == 0 {
return false, fmt.Errorf("missing node")
}
nodeID = nodes[0].ID
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})

// Query to check the node status
args := []string{"-address=" + url}
if tt.verbose {
args = append(args, "-verbose")
}
args = append(args, nodeID)

if code := cmd.Run(args); code != 0 {
t.Fatalf("expected exit 0, got: %d", code)
}

out := ui.OutputWriter.String()

tt.assertions(out)

ui.OutputWriter.Reset()
})
}

}
10 changes: 10 additions & 0 deletions nomad/structs/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,3 +616,13 @@ type ClientHostNetworkConfig struct {
Interface string `hcl:"interface"`
ReservedPorts string `hcl:"reserved_ports"`
}

func (p *ClientHostNetworkConfig) Copy() *ClientHostNetworkConfig {
if p == nil {
return nil
}

c := new(ClientHostNetworkConfig)
*c = *p
return c
}
19 changes: 19 additions & 0 deletions nomad/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1908,6 +1908,9 @@ type Node struct {
// HostVolumes is a map of host volume names to their configuration
HostVolumes map[string]*ClientHostVolumeConfig

// HostNetworks is a map of host host_network names to their configuration
HostNetworks map[string]*ClientHostNetworkConfig

// LastDrain contains metadata about the most recent drain operation
LastDrain *DrainMetadata

Expand Down Expand Up @@ -1993,6 +1996,7 @@ func (n *Node) Copy() *Node {
nn.CSINodePlugins = copyNodeCSI(nn.CSINodePlugins)
nn.Drivers = copyNodeDrivers(n.Drivers)
nn.HostVolumes = copyNodeHostVolumes(n.HostVolumes)
nn.HostNetworks = copyNodeHostNetworks(n.HostNetworks)
return nn
}

Expand Down Expand Up @@ -2054,6 +2058,21 @@ func copyNodeHostVolumes(volumes map[string]*ClientHostVolumeConfig) map[string]
return c
}

// copyNodeHostVolumes is a helper to copy a map of string to HostNetwork
func copyNodeHostNetworks(networks map[string]*ClientHostNetworkConfig) map[string]*ClientHostNetworkConfig {
l := len(networks)
if l == 0 {
return nil
}

c := make(map[string]*ClientHostNetworkConfig, l)
for network, v := range networks {
c[network] = v.Copy()
}

return c
}

// TerminalStatus returns if the current status is terminal and
// will no longer transition.
func (n *Node) TerminalStatus() bool {
Expand Down
7 changes: 7 additions & 0 deletions website/content/api-docs/nodes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,13 @@ $ curl \
"ReadOnly": false
}
},
"HostNetworks" : {
"public": {
"Name": "public",
"CIDR": "10.199.0.200/24",
"ReservedPorts": "8080,8081"
}
}
"ID": "1ac61e33-a465-2ace-f63f-cffa1285e7eb",
"LastDrain": {
"AccessorID": "4e1b7ce1-f8aa-d7ff-09f1-55c3a0fd3988",
Expand Down

0 comments on commit 9a5248b

Please sign in to comment.