github.com/hernad/nomad@v1.6.112/command/volume_status_csi.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/hernad/nomad/api"
    14  )
    15  
    16  func (c *VolumeStatusCommand) csiBanner() {
    17  	if !(c.json || len(c.template) > 0) {
    18  		c.Ui.Output(c.Colorize().Color("[bold]Container Storage Interface[reset]"))
    19  	}
    20  }
    21  
    22  func (c *VolumeStatusCommand) csiStatus(client *api.Client, id string) int {
    23  	// Invoke list mode if no volume id
    24  	if id == "" {
    25  		return c.listVolumes(client)
    26  	}
    27  
    28  	// Prefix search for the volume
    29  	vols, _, err := client.CSIVolumes().List(&api.QueryOptions{Prefix: id})
    30  	if err != nil {
    31  		c.Ui.Error(fmt.Sprintf("Error querying volumes: %s", err))
    32  		return 1
    33  	}
    34  	if len(vols) == 0 {
    35  		c.Ui.Error(fmt.Sprintf("No volumes(s) with prefix or ID %q found", id))
    36  		return 1
    37  	}
    38  
    39  	var ns string
    40  
    41  	if len(vols) == 1 {
    42  		// need to set id from the actual ID because it might be a prefix
    43  		id = vols[0].ID
    44  		ns = vols[0].Namespace
    45  	}
    46  
    47  	// List sorts by CreateIndex, not by ID, so we need to search for
    48  	// exact matches but account for multiple exact ID matches across
    49  	// namespaces
    50  	if len(vols) > 1 {
    51  		exactMatchesCount := 0
    52  		for _, vol := range vols {
    53  			if vol.ID == id {
    54  				exactMatchesCount++
    55  				ns = vol.Namespace
    56  			}
    57  		}
    58  		if exactMatchesCount != 1 {
    59  			out, err := c.csiFormatVolumes(vols)
    60  			if err != nil {
    61  				c.Ui.Error(fmt.Sprintf("Error formatting: %s", err))
    62  				return 1
    63  			}
    64  			c.Ui.Error(fmt.Sprintf("Prefix matched multiple volumes\n\n%s", out))
    65  			return 1
    66  		}
    67  	}
    68  
    69  	// Try querying the volume
    70  	client.SetNamespace(ns)
    71  	vol, _, err := client.CSIVolumes().Info(id, nil)
    72  	if err != nil {
    73  		c.Ui.Error(fmt.Sprintf("Error querying volume: %s", err))
    74  		return 1
    75  	}
    76  
    77  	str, err := c.formatBasic(vol)
    78  	if err != nil {
    79  		c.Ui.Error(fmt.Sprintf("Error formatting volume: %s", err))
    80  		return 1
    81  	}
    82  	c.Ui.Output(str)
    83  
    84  	return 0
    85  }
    86  
    87  func (c *VolumeStatusCommand) listVolumes(client *api.Client) int {
    88  
    89  	c.csiBanner()
    90  	vols, _, err := client.CSIVolumes().List(nil)
    91  	if err != nil {
    92  		c.Ui.Error(fmt.Sprintf("Error querying volumes: %s", err))
    93  		return 1
    94  	}
    95  
    96  	if len(vols) == 0 {
    97  		// No output if we have no volumes
    98  		c.Ui.Error("No CSI volumes")
    99  	} else {
   100  		str, err := c.csiFormatVolumes(vols)
   101  		if err != nil {
   102  			c.Ui.Error(fmt.Sprintf("Error formatting: %s", err))
   103  			return 1
   104  		}
   105  		c.Ui.Output(str)
   106  	}
   107  	if !c.verbose {
   108  		return 0
   109  	}
   110  
   111  	plugins, _, err := client.CSIPlugins().List(nil)
   112  	if err != nil {
   113  		c.Ui.Error(fmt.Sprintf("Error querying CSI plugins: %s", err))
   114  		return 1
   115  	}
   116  
   117  	if len(plugins) == 0 {
   118  		return 0 // No more output if we have no plugins
   119  	}
   120  
   121  	var code int
   122  	q := &api.QueryOptions{PerPage: 30} // TODO: tune page size
   123  
   124  NEXT_PLUGIN:
   125  	for _, plugin := range plugins {
   126  		if !plugin.ControllerRequired || plugin.ControllersHealthy < 1 {
   127  			continue // only controller plugins can support this query
   128  		}
   129  		for {
   130  			externalList, _, err := client.CSIVolumes().ListExternal(plugin.ID, q)
   131  			if err != nil && !errors.Is(err, io.EOF) {
   132  				c.Ui.Error(fmt.Sprintf(
   133  					"Error querying CSI external volumes for plugin %q: %s", plugin.ID, err))
   134  				// we'll stop querying this plugin, but there may be more to
   135  				// query, so report and set the error code but move on to the
   136  				// next plugin
   137  				code = 1
   138  				continue NEXT_PLUGIN
   139  			}
   140  			if externalList == nil || len(externalList.Volumes) == 0 {
   141  				// several plugins return EOF once you hit the end of the page,
   142  				// rather than an empty list
   143  				continue NEXT_PLUGIN
   144  			}
   145  			rows := []string{"External ID|Condition|Nodes"}
   146  			for _, v := range externalList.Volumes {
   147  				condition := "OK"
   148  				if v.IsAbnormal {
   149  					condition = fmt.Sprintf("Abnormal (%v)", v.Status)
   150  				}
   151  				rows = append(rows, fmt.Sprintf("%s|%s|%s",
   152  					limit(v.ExternalID, c.length),
   153  					limit(condition, 20),
   154  					strings.Join(v.PublishedExternalNodeIDs, ","),
   155  				))
   156  			}
   157  			c.Ui.Output(formatList(rows))
   158  
   159  			q.NextToken = externalList.NextToken
   160  			if q.NextToken == "" {
   161  				break
   162  			}
   163  			// we can't know the shape of arbitrarily-sized lists of volumes,
   164  			// so break after each page
   165  			c.Ui.Output("...")
   166  		}
   167  	}
   168  
   169  	return code
   170  }
   171  
   172  func (c *VolumeStatusCommand) csiFormatVolumes(vols []*api.CSIVolumeListStub) (string, error) {
   173  	// Sort the output by volume id
   174  	sort.Slice(vols, func(i, j int) bool { return vols[i].ID < vols[j].ID })
   175  
   176  	if c.json || len(c.template) > 0 {
   177  		out, err := Format(c.json, c.template, vols)
   178  		if err != nil {
   179  			return "", fmt.Errorf("format error: %v", err)
   180  		}
   181  		return out, nil
   182  	}
   183  
   184  	return csiFormatSortedVolumes(vols)
   185  }
   186  
   187  // Format the volumes, assumes that we're already sorted by volume ID
   188  func csiFormatSortedVolumes(vols []*api.CSIVolumeListStub) (string, error) {
   189  	rows := make([]string, len(vols)+1)
   190  	rows[0] = "ID|Name|Namespace|Plugin ID|Schedulable|Access Mode"
   191  	for i, v := range vols {
   192  		rows[i+1] = fmt.Sprintf("%s|%s|%s|%s|%t|%s",
   193  			v.ID,
   194  			v.Name,
   195  			v.Namespace,
   196  			v.PluginID,
   197  			v.Schedulable,
   198  			v.AccessMode,
   199  		)
   200  	}
   201  	return formatList(rows), nil
   202  }
   203  
   204  func (c *VolumeStatusCommand) formatBasic(vol *api.CSIVolume) (string, error) {
   205  	if c.json || len(c.template) > 0 {
   206  		out, err := Format(c.json, c.template, vol)
   207  		if err != nil {
   208  			return "", fmt.Errorf("format error: %v", err)
   209  		}
   210  		return out, nil
   211  	}
   212  
   213  	output := []string{
   214  		fmt.Sprintf("ID|%s", vol.ID),
   215  		fmt.Sprintf("Name|%s", vol.Name),
   216  		fmt.Sprintf("Namespace|%s", vol.Namespace),
   217  		fmt.Sprintf("External ID|%s", vol.ExternalID),
   218  		fmt.Sprintf("Plugin ID|%s", vol.PluginID),
   219  		fmt.Sprintf("Provider|%s", vol.Provider),
   220  		fmt.Sprintf("Version|%s", vol.ProviderVersion),
   221  		fmt.Sprintf("Schedulable|%t", vol.Schedulable),
   222  		fmt.Sprintf("Controllers Healthy|%d", vol.ControllersHealthy),
   223  		fmt.Sprintf("Controllers Expected|%d", vol.ControllersExpected),
   224  		fmt.Sprintf("Nodes Healthy|%d", vol.NodesHealthy),
   225  		fmt.Sprintf("Nodes Expected|%d", vol.NodesExpected),
   226  
   227  		fmt.Sprintf("Access Mode|%s", vol.AccessMode),
   228  		fmt.Sprintf("Attachment Mode|%s", vol.AttachmentMode),
   229  		fmt.Sprintf("Mount Options|%s", csiVolMountOption(vol.MountOptions, nil)),
   230  		fmt.Sprintf("Namespace|%s", vol.Namespace),
   231  	}
   232  
   233  	// Exit early
   234  	if c.short {
   235  		return formatKV(output), nil
   236  	}
   237  
   238  	full := []string{formatKV(output)}
   239  
   240  	if len(vol.Topologies) > 0 {
   241  		topoBanner := c.Colorize().Color("\n[bold]Topologies[reset]")
   242  		topo := c.formatTopology(vol)
   243  		full = append(full, topoBanner)
   244  		full = append(full, topo)
   245  	}
   246  
   247  	// Format the allocs
   248  	banner := c.Colorize().Color("\n[bold]Allocations[reset]")
   249  	allocs := formatAllocListStubs(vol.Allocations, c.verbose, c.length)
   250  	full = append(full, banner)
   251  	full = append(full, allocs)
   252  
   253  	return strings.Join(full, "\n"), nil
   254  }
   255  
   256  func (c *VolumeStatusCommand) formatTopology(vol *api.CSIVolume) string {
   257  	rows := []string{"Topology|Segments"}
   258  	for i, t := range vol.Topologies {
   259  		if t == nil {
   260  			continue
   261  		}
   262  		segmentPairs := make([]string, 0, len(t.Segments))
   263  		for k, v := range t.Segments {
   264  			segmentPairs = append(segmentPairs, fmt.Sprintf("%s=%s", k, v))
   265  		}
   266  		// note: this looks awkward because we don't have any other
   267  		// place where we list collections of arbitrary k/v's like
   268  		// this without just dumping JSON formatted outputs. It's likely
   269  		// the spec will expand to add extra fields, in which case we'll
   270  		// add them here and drop the first column
   271  		rows = append(rows, fmt.Sprintf("%02d|%v", i, strings.Join(segmentPairs, ", ")))
   272  	}
   273  	if len(rows) == 1 {
   274  		return ""
   275  	}
   276  	return formatList(rows)
   277  }
   278  
   279  func csiVolMountOption(volume, request *api.CSIMountOptions) string {
   280  	var req, opts *api.CSIMountOptions
   281  
   282  	if request != nil {
   283  		req = &api.CSIMountOptions{
   284  			FSType:     request.FSType,
   285  			MountFlags: request.MountFlags,
   286  		}
   287  	}
   288  
   289  	if volume == nil {
   290  		opts = req
   291  	} else {
   292  		opts = &api.CSIMountOptions{
   293  			FSType:     volume.FSType,
   294  			MountFlags: volume.MountFlags,
   295  		}
   296  		opts.Merge(req)
   297  	}
   298  
   299  	if opts == nil {
   300  		return "<none>"
   301  	}
   302  
   303  	var out string
   304  	if opts.FSType != "" {
   305  		out = fmt.Sprintf("fs_type: %s", opts.FSType)
   306  	}
   307  
   308  	if len(opts.MountFlags) > 0 {
   309  		out = fmt.Sprintf("%s flags: %s", out, strings.Join(opts.MountFlags, ", "))
   310  	}
   311  
   312  	return out
   313  }