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
|
||||
}
|
||||
|
||||
//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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue