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