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 }