open-nomad/command/plugin_status_csi.go
Tim Gross 13ea2c7fb3
CSI: display plugin capabilities in verbose status (#12116)
The behaviors of CSI plugins are governed by their capabilities as
defined by the CSI specification. When debugging plugin issues, it's
useful to know which behaviors are expected so they can be matched
against RPC calls made to the plugin allocations.

Expose the plugin capabilities as named in the CSI spec in the `nomad
plugin status -verbose` output.
2022-02-24 13:51:38 -05:00

210 lines
5.2 KiB
Go

package command
import (
"fmt"
"sort"
"strings"
"github.com/hashicorp/nomad/api"
)
func (c *PluginStatusCommand) csiBanner() {
if !(c.json || len(c.template) > 0) {
c.Ui.Output(c.Colorize().Color("[bold]Container Storage Interface[reset]"))
}
}
func (c *PluginStatusCommand) csiStatus(client *api.Client, id string) int {
if id == "" {
c.csiBanner()
plugs, _, err := client.CSIPlugins().List(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying CSI plugins: %s", err))
return 1
}
if len(plugs) == 0 {
// No output if we have no plugins
c.Ui.Error("No CSI plugins")
} else {
str, err := c.csiFormatPlugins(plugs)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting: %s", err))
return 1
}
c.Ui.Output(str)
}
return 0
}
// Lookup matched a single plugin
plug, _, err := client.CSIPlugins().Info(id, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying plugin: %s", err))
return 1
}
str, err := c.csiFormatPlugin(plug)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting plugin: %s", err))
return 1
}
c.Ui.Output(str)
return 0
}
func (c *PluginStatusCommand) csiFormatPlugins(plugs []*api.CSIPluginListStub) (string, error) {
// Sort the output by quota name
sort.Slice(plugs, func(i, j int) bool { return plugs[i].ID < plugs[j].ID })
if c.json || len(c.template) > 0 {
out, err := Format(c.json, c.template, plugs)
if err != nil {
return "", fmt.Errorf("format error: %v", err)
}
return out, nil
}
rows := make([]string, len(plugs)+1)
rows[0] = "ID|Provider|Controllers Healthy/Expected|Nodes Healthy/Expected"
for i, p := range plugs {
rows[i+1] = fmt.Sprintf("%s|%s|%d/%d|%d/%d",
limit(p.ID, c.length),
p.Provider,
p.ControllersHealthy,
p.ControllersExpected,
p.NodesHealthy,
p.NodesExpected,
)
}
return formatList(rows), nil
}
func (c *PluginStatusCommand) csiFormatPlugin(plug *api.CSIPlugin) (string, error) {
if c.json || len(c.template) > 0 {
out, err := Format(c.json, c.template, plug)
if err != nil {
return "", fmt.Errorf("format error: %v", err)
}
return out, nil
}
output := []string{
fmt.Sprintf("ID|%s", plug.ID),
fmt.Sprintf("Provider|%s", plug.Provider),
fmt.Sprintf("Version|%s", plug.Version),
fmt.Sprintf("Controllers Healthy|%d", plug.ControllersHealthy),
fmt.Sprintf("Controllers Expected|%d", plug.ControllersExpected),
fmt.Sprintf("Nodes Healthy|%d", plug.NodesHealthy),
fmt.Sprintf("Nodes Expected|%d", plug.NodesExpected),
}
// Exit early
if c.short {
return formatKV(output), nil
}
full := []string{formatKV(output)}
if c.verbose {
controllerCaps := c.formatControllerCaps(plug.Controllers)
if controllerCaps != "" {
full = append(full, c.Colorize().Color("\n[bold]Controller Capabilities[reset]"))
full = append(full, controllerCaps)
}
nodeCaps := c.formatNodeCaps(plug.Nodes)
if nodeCaps != "" {
full = append(full, c.Colorize().Color("\n[bold]Node Capabilities[reset]"))
full = append(full, nodeCaps)
}
}
// Format the allocs
banner := c.Colorize().Color("\n[bold]Allocations[reset]")
allocs := formatAllocListStubs(plug.Allocations, c.verbose, c.length)
full = append(full, banner)
full = append(full, allocs)
return strings.Join(full, "\n"), nil
}
func (c *PluginStatusCommand) formatControllerCaps(controllers map[string]*api.CSIInfo) string {
caps := []string{}
for _, controller := range controllers {
switch info := controller.ControllerInfo; {
case info.SupportsCreateDelete:
caps = append(caps, "CREATE_DELETE_VOLUME")
fallthrough
case info.SupportsAttachDetach:
caps = append(caps, "CONTROLLER_ATTACH_DETACH")
fallthrough
case info.SupportsListVolumes:
caps = append(caps, "LIST_VOLUMES")
fallthrough
case info.SupportsGetCapacity:
caps = append(caps, "GET_CAPACITY")
fallthrough
case info.SupportsCreateDeleteSnapshot:
caps = append(caps, "CREATE_DELETE_SNAPSHOT")
fallthrough
case info.SupportsListSnapshots:
caps = append(caps, "CREATE_LIST_SNAPSHOTS")
fallthrough
case info.SupportsClone:
caps = append(caps, "CLONE_VOLUME")
fallthrough
case info.SupportsReadOnlyAttach:
caps = append(caps, "ATTACH_READONLY")
fallthrough
case info.SupportsExpand:
caps = append(caps, "EXPAND_VOLUME")
fallthrough
case info.SupportsListVolumesAttachedNodes:
caps = append(caps, "LIST_VOLUMES_PUBLISHED_NODES")
fallthrough
case info.SupportsCondition:
caps = append(caps, "VOLUME_CONDITION")
fallthrough
case info.SupportsGet:
caps = append(caps, "GET_VOLUME")
fallthrough
default:
}
break
}
if len(caps) == 0 {
return ""
}
return strings.Join(caps, "\n\t")
}
func (c *PluginStatusCommand) formatNodeCaps(nodes map[string]*api.CSIInfo) string {
caps := []string{}
for _, node := range nodes {
switch info := node.NodeInfo; {
case info.RequiresNodeStageVolume:
caps = append(caps, "STAGE_UNSTAGE_VOLUME")
fallthrough
case info.SupportsStats:
caps = append(caps, "GET_VOLUME_STATS")
fallthrough
case info.SupportsExpand:
caps = append(caps, "EXPAND_VOLUME")
fallthrough
case info.SupportsCondition:
caps = append(caps, "VOLUME_CONDITION")
fallthrough
default:
}
break
}
if len(caps) == 0 {
return ""
}
return " " + strings.Join(caps, "\n ")
}