cli: show `host_network` in `nomad status` (#11432)

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>
This commit is contained in:
Alessandro De Blasis 2021-11-05 13:02:46 +00:00 committed by GitHub
parent 503f201415
commit 07c670fdc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 213 additions and 0 deletions

3
.changelog/11432.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
cli: the command `node status` now returns `host_network` information as well
```

View File

@ -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.
@ -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

View File

@ -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

View File

@ -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)
@ -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)))
@ -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)
@ -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)
}
@ -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

View File

@ -2,6 +2,7 @@ package command
import (
"fmt"
"regexp"
"testing"
"github.com/hashicorp/nomad/command/agent"
@ -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) {
@ -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()
})
}
}

View File

@ -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
}

View File

@ -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
@ -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
}
@ -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 {

View File

@ -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",