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:
parent
503f201415
commit
07c670fdc0
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
cli: the command `node status` now returns `host_network` information as well
|
||||||
|
```
|
|
@ -506,6 +506,14 @@ type HostVolumeInfo struct {
|
||||||
ReadOnly bool
|
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
|
type DrainStatus string
|
||||||
|
|
||||||
// DrainMetadata contains information about the most recent drain operation for a given Node.
|
// DrainMetadata contains information about the most recent drain operation for a given Node.
|
||||||
|
@ -541,6 +549,7 @@ type Node struct {
|
||||||
Events []*NodeEvent
|
Events []*NodeEvent
|
||||||
Drivers map[string]*DriverInfo
|
Drivers map[string]*DriverInfo
|
||||||
HostVolumes map[string]*HostVolumeInfo
|
HostVolumes map[string]*HostVolumeInfo
|
||||||
|
HostNetworks map[string]*HostNetworkInfo
|
||||||
CSIControllerPlugins map[string]*CSIInfo
|
CSIControllerPlugins map[string]*CSIInfo
|
||||||
CSINodePlugins map[string]*CSIInfo
|
CSINodePlugins map[string]*CSIInfo
|
||||||
LastDrain *DrainMetadata
|
LastDrain *DrainMetadata
|
||||||
|
|
|
@ -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 == "" {
|
if node.Name == "" {
|
||||||
node.Name = node.ID
|
node.Name = node.ID
|
||||||
|
|
|
@ -349,6 +349,16 @@ func nodeVolumeNames(n *api.Node) []string {
|
||||||
return volumes
|
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 {
|
func formatDrain(n *api.Node) string {
|
||||||
if n.DrainStrategy != nil {
|
if n.DrainStrategy != nil {
|
||||||
b := new(strings.Builder)
|
b := new(strings.Builder)
|
||||||
|
@ -400,6 +410,7 @@ func (c *NodeStatusCommand) formatNode(client *api.Client, node *api.Node) int {
|
||||||
|
|
||||||
if c.short {
|
if c.short {
|
||||||
basic = append(basic, fmt.Sprintf("Host Volumes|%s", strings.Join(nodeVolumeNames(node), ",")))
|
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("CSI Volumes|%s", strings.Join(nodeCSIVolumeNames(node, runningAllocs), ",")))
|
||||||
basic = append(basic, fmt.Sprintf("Drivers|%s", strings.Join(nodeDrivers(node), ",")))
|
basic = append(basic, fmt.Sprintf("Drivers|%s", strings.Join(nodeDrivers(node), ",")))
|
||||||
c.Ui.Output(c.Colorize().Color(formatKV(basic)))
|
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
|
// driver info in the basic output
|
||||||
if !c.verbose {
|
if !c.verbose {
|
||||||
basic = append(basic, fmt.Sprintf("Host Volumes|%s", strings.Join(nodeVolumeNames(node), ",")))
|
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("CSI Volumes|%s", strings.Join(nodeCSIVolumeNames(node, runningAllocs), ",")))
|
||||||
driverStatus := fmt.Sprintf("Driver Status| %s", c.outputTruncatedNodeDriverInfo(node))
|
driverStatus := fmt.Sprintf("Driver Status| %s", c.outputTruncatedNodeDriverInfo(node))
|
||||||
basic = append(basic, driverStatus)
|
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 we're running in verbose mode, include full host volume and driver info
|
||||||
if c.verbose {
|
if c.verbose {
|
||||||
c.outputNodeVolumeInfo(node)
|
c.outputNodeVolumeInfo(node)
|
||||||
|
c.outputNodeNetworkInfo(node)
|
||||||
c.outputNodeCSIVolumeInfo(client, node, runningAllocs)
|
c.outputNodeCSIVolumeInfo(client, node, runningAllocs)
|
||||||
c.outputNodeDriverInfo(node)
|
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) {
|
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
|
// Duplicate nodeCSIVolumeNames to sort by name but also index volume names to ids
|
||||||
|
|
|
@ -2,6 +2,7 @@ package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/nomad/command/agent"
|
"github.com/hashicorp/nomad/command/agent"
|
||||||
|
@ -11,6 +12,7 @@ import (
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
"github.com/posener/complete"
|
"github.com/posener/complete"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStatusCommand_Run_JobStatus(t *testing.T) {
|
func TestStatusCommand_Run_JobStatus(t *testing.T) {
|
||||||
|
@ -233,3 +235,124 @@ func TestStatusCommand_AutocompleteArgs(t *testing.T) {
|
||||||
res := predictor.Predict(args)
|
res := predictor.Predict(args)
|
||||||
assert.Contains(res, job.ID)
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -616,3 +616,13 @@ type ClientHostNetworkConfig struct {
|
||||||
Interface string `hcl:"interface"`
|
Interface string `hcl:"interface"`
|
||||||
ReservedPorts string `hcl:"reserved_ports"`
|
ReservedPorts string `hcl:"reserved_ports"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *ClientHostNetworkConfig) Copy() *ClientHostNetworkConfig {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(ClientHostNetworkConfig)
|
||||||
|
*c = *p
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
|
@ -1908,6 +1908,9 @@ type Node struct {
|
||||||
// HostVolumes is a map of host volume names to their configuration
|
// HostVolumes is a map of host volume names to their configuration
|
||||||
HostVolumes map[string]*ClientHostVolumeConfig
|
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 contains metadata about the most recent drain operation
|
||||||
LastDrain *DrainMetadata
|
LastDrain *DrainMetadata
|
||||||
|
|
||||||
|
@ -1993,6 +1996,7 @@ func (n *Node) Copy() *Node {
|
||||||
nn.CSINodePlugins = copyNodeCSI(nn.CSINodePlugins)
|
nn.CSINodePlugins = copyNodeCSI(nn.CSINodePlugins)
|
||||||
nn.Drivers = copyNodeDrivers(n.Drivers)
|
nn.Drivers = copyNodeDrivers(n.Drivers)
|
||||||
nn.HostVolumes = copyNodeHostVolumes(n.HostVolumes)
|
nn.HostVolumes = copyNodeHostVolumes(n.HostVolumes)
|
||||||
|
nn.HostNetworks = copyNodeHostNetworks(n.HostNetworks)
|
||||||
return nn
|
return nn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2054,6 +2058,21 @@ func copyNodeHostVolumes(volumes map[string]*ClientHostVolumeConfig) map[string]
|
||||||
return c
|
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
|
// TerminalStatus returns if the current status is terminal and
|
||||||
// will no longer transition.
|
// will no longer transition.
|
||||||
func (n *Node) TerminalStatus() bool {
|
func (n *Node) TerminalStatus() bool {
|
||||||
|
|
|
@ -296,6 +296,13 @@ $ curl \
|
||||||
"ReadOnly": false
|
"ReadOnly": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"HostNetworks" : {
|
||||||
|
"public": {
|
||||||
|
"Name": "public",
|
||||||
|
"CIDR": "10.199.0.200/24",
|
||||||
|
"ReservedPorts": "8080,8081"
|
||||||
|
}
|
||||||
|
}
|
||||||
"ID": "1ac61e33-a465-2ace-f63f-cffa1285e7eb",
|
"ID": "1ac61e33-a465-2ace-f63f-cffa1285e7eb",
|
||||||
"LastDrain": {
|
"LastDrain": {
|
||||||
"AccessorID": "4e1b7ce1-f8aa-d7ff-09f1-55c3a0fd3988",
|
"AccessorID": "4e1b7ce1-f8aa-d7ff-09f1-55c3a0fd3988",
|
||||||
|
|
Loading…
Reference in New Issue