github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/status/output_tabular.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package status 5 6 import ( 7 "fmt" 8 "io" 9 "regexp" 10 "sort" 11 "strings" 12 13 "github.com/juju/ansiterm" 14 "github.com/juju/errors" 15 "github.com/juju/naturalsort" 16 "gopkg.in/juju/charm.v6" 17 "gopkg.in/juju/charm.v6/hooks" 18 19 cmdcrossmodel "github.com/juju/juju/cmd/juju/crossmodel" 20 "github.com/juju/juju/cmd/juju/storage" 21 "github.com/juju/juju/cmd/output" 22 "github.com/juju/juju/core/crossmodel" 23 "github.com/juju/juju/core/instance" 24 "github.com/juju/juju/core/relation" 25 "github.com/juju/juju/core/status" 26 ) 27 28 const ( 29 caasModelType = "caas" 30 ellipsis = "..." 31 iaasMaxVersionWidth = 15 32 caasMaxVersionWidth = 30 33 ) 34 35 // FormatTabular writes a tabular summary of machines, applications, and 36 // units. Any subordinate items are indented by two spaces beneath 37 // their superior. 38 func FormatTabular(writer io.Writer, forceColor bool, value interface{}) error { 39 fs, valueConverted := value.(formattedStatus) 40 if !valueConverted { 41 return errors.Errorf("expected value of type %T, got %T", fs, value) 42 } 43 44 // To format things into columns. 45 tw := output.TabWriter(writer) 46 if forceColor { 47 tw.SetColorCapable(forceColor) 48 } 49 50 cloudRegion := fs.Model.Cloud 51 if fs.Model.CloudRegion != "" { 52 cloudRegion += "/" + fs.Model.CloudRegion 53 } 54 55 // Default table output 56 header := []interface{}{"Model", "Controller", "Cloud/Region", "Version"} 57 values := []interface{}{fs.Model.Name, fs.Model.Controller, cloudRegion, fs.Model.Version} 58 59 // Optional table output if values exist 60 message := getModelMessage(fs.Model) 61 if fs.Model.SLA != "" { 62 header = append(header, "SLA") 63 values = append(values, fs.Model.SLA) 64 } 65 if cs := fs.Controller; cs != nil && cs.Timestamp != "" { 66 header = append(header, "Timestamp") 67 values = append(values, cs.Timestamp) 68 } 69 if message != "" { 70 header = append(header, "Notes") 71 values = append(values, message) 72 } 73 74 // The first set of headers don't use outputHeaders because it adds the blank line. 75 w := startSection(tw, true, header...) 76 w.Println(values...) 77 78 if len(fs.RemoteApplications) > 0 { 79 printRemoteApplications(tw, fs.RemoteApplications) 80 } 81 82 if len(fs.Applications) > 0 { 83 printApplications(tw, fs) 84 } 85 86 if fs.Model.Type != caasModelType && len(fs.Machines) > 0 { 87 printMachines(tw, false, fs.Machines) 88 } 89 90 if err := printOffers(tw, fs.Offers); err != nil { 91 w.Println(err.Error()) 92 } 93 94 if len(fs.Relations) > 0 { 95 printRelations(tw, fs.Relations) 96 } 97 98 if fs.Storage != nil { 99 storage.FormatStorageListForStatusTabular(tw, *fs.Storage) 100 } 101 102 endSection(tw) 103 return nil 104 } 105 106 func startSection(tw *ansiterm.TabWriter, top bool, headers ...interface{}) output.Wrapper { 107 w := output.Wrapper{tw} 108 if !top { 109 w.Println() 110 } 111 w.Println(headers...) 112 return w 113 } 114 115 func endSection(tw *ansiterm.TabWriter) { 116 tw.Flush() 117 } 118 119 func printApplications(tw *ansiterm.TabWriter, fs formattedStatus) { 120 maxVersionWidth := iaasMaxVersionWidth 121 if fs.Model.Type == caasModelType { 122 maxVersionWidth = caasMaxVersionWidth 123 } 124 truncatedWidth := maxVersionWidth - len(ellipsis) 125 126 metering := fs.Model.MeterStatus != nil 127 units := make(map[string]unitStatus) 128 var w output.Wrapper 129 if fs.Model.Type == caasModelType { 130 w = startSection(tw, false, "App", "Version", "Status", "Scale", "Charm", "Store", "Rev", "OS", "Address", "Notes") 131 } else { 132 w = startSection(tw, false, "App", "Version", "Status", "Scale", "Charm", "Store", "Rev", "OS", "Notes") 133 } 134 tw.SetColumnAlignRight(3) 135 tw.SetColumnAlignRight(6) 136 for _, appName := range naturalsort.Sort(stringKeysFromMap(fs.Applications)) { 137 app := fs.Applications[appName] 138 version := app.Version 139 // CAAS versions may have repo prefix we don't care about. 140 if fs.Model.Type == caasModelType { 141 parts := strings.Split(version, "/") 142 if len(parts) == 2 { 143 version = parts[1] 144 } 145 } 146 // Don't let a long version push out the version column. 147 if len(version) > maxVersionWidth { 148 version = version[:truncatedWidth] + ellipsis 149 } 150 // Notes may well contain other things later. 151 notes := "" 152 if app.Exposed { 153 notes = "exposed" 154 } 155 // Expose any operator messages. 156 if fs.Model.Type == caasModelType { 157 if app.StatusInfo.Message != "" { 158 notes = app.StatusInfo.Message 159 } 160 } 161 w.Print(appName, version) 162 w.PrintStatus(app.StatusInfo.Current) 163 scale, warn := fs.applicationScale(appName) 164 if warn { 165 w.PrintColor(output.WarningHighlight, scale) 166 } else { 167 w.Print(scale) 168 } 169 170 w.Print(app.CharmName, 171 app.CharmOrigin, 172 app.CharmRev, 173 app.OS) 174 if fs.Model.Type == caasModelType { 175 w.Print(app.Address) 176 } 177 178 w.Println(notes) 179 for un, u := range app.Units { 180 units[un] = u 181 if u.MeterStatus != nil { 182 metering = true 183 } 184 } 185 } 186 endSection(tw) 187 188 pUnit := func(name string, u unitStatus, level int) { 189 message := u.WorkloadStatusInfo.Message 190 // If we're still allocating and there's a message, show that. 191 if u.JujuStatusInfo.Current == status.Allocating && message == "" { 192 message = u.JujuStatusInfo.Message 193 } 194 agentDoing := agentDoing(u.JujuStatusInfo) 195 if agentDoing != "" { 196 message = fmt.Sprintf("(%s) %s", agentDoing, message) 197 } 198 if u.Leader { 199 name += "*" 200 } 201 w.Print(indent("", level*2, name)) 202 w.PrintStatus(u.WorkloadStatusInfo.Current) 203 w.PrintStatus(u.JujuStatusInfo.Current) 204 if fs.Model.Type == caasModelType { 205 w.Println( 206 u.Address, 207 strings.Join(u.OpenedPorts, ","), 208 message, 209 ) 210 return 211 } 212 w.Println( 213 u.Machine, 214 u.PublicAddress, 215 strings.Join(u.OpenedPorts, ","), 216 message, 217 ) 218 } 219 220 if len(units) > 0 { 221 if fs.Model.Type == caasModelType { 222 startSection(tw, false, "Unit", "Workload", "Agent", "Address", "Ports", "Message") 223 } else { 224 startSection(tw, false, "Unit", "Workload", "Agent", "Machine", "Public address", "Ports", "Message") 225 } 226 for _, name := range naturalsort.Sort(stringKeysFromMap(units)) { 227 u := units[name] 228 pUnit(name, u, 0) 229 const indentationLevel = 1 230 recurseUnits(u, indentationLevel, pUnit) 231 } 232 endSection(tw) 233 } 234 235 if !metering { 236 return 237 } 238 239 startSection(tw, false, "Entity", "Meter status", "Message") 240 if fs.Model.MeterStatus != nil { 241 w.Print("model") 242 outputColor := fromMeterStatusColor(fs.Model.MeterStatus.Color) 243 w.PrintColor(outputColor, fs.Model.MeterStatus.Color) 244 w.PrintColor(outputColor, fs.Model.MeterStatus.Message) 245 w.Println() 246 } 247 for _, name := range naturalsort.Sort(stringKeysFromMap(units)) { 248 u := units[name] 249 if u.MeterStatus != nil { 250 w.Print(name) 251 outputColor := fromMeterStatusColor(u.MeterStatus.Color) 252 w.PrintColor(outputColor, u.MeterStatus.Color) 253 w.PrintColor(outputColor, u.MeterStatus.Message) 254 w.Println() 255 } 256 } 257 endSection(tw) 258 } 259 260 func printRemoteApplications(tw *ansiterm.TabWriter, remoteApplications map[string]remoteApplicationStatus) { 261 w := startSection(tw, false, "SAAS", "Status", "Store", "URL") 262 for _, appName := range naturalsort.Sort(stringKeysFromMap(remoteApplications)) { 263 app := remoteApplications[appName] 264 var store, urlPath string 265 url, err := crossmodel.ParseOfferURL(app.OfferURL) 266 if err == nil { 267 store = url.Source 268 url.Source = "" 269 urlPath = url.Path() 270 if store == "" { 271 store = "local" 272 } 273 } else { 274 // This is not expected. 275 logger.Errorf("invalid offer URL %q: %v", app.OfferURL, err) 276 store = "unknown" 277 urlPath = app.OfferURL 278 } 279 w.Print(appName) 280 w.PrintStatus(app.StatusInfo.Current) 281 w.Println(store, urlPath) 282 } 283 endSection(tw) 284 } 285 286 func printRelations(tw *ansiterm.TabWriter, relations []relationStatus) { 287 sort.Slice(relations, func(i, j int) bool { 288 a, b := relations[i], relations[j] 289 if a.Provider == b.Provider { 290 return a.Requirer < b.Requirer 291 } 292 return a.Provider < b.Provider 293 }) 294 295 w := startSection(tw, false, "Relation provider", "Requirer", "Interface", "Type", "Message") 296 297 for _, r := range relations { 298 w.Print(r.Provider, r.Requirer, r.Interface, r.Type) 299 if r.Status != string(relation.Joined) { 300 w.PrintColor(cmdcrossmodel.RelationStatusColor(relation.Status(r.Status)), r.Status) 301 if r.Message != "" { 302 w.Print(" - " + r.Message) 303 } 304 } 305 w.Println() 306 } 307 endSection(tw) 308 } 309 310 type offerItems []offerStatus 311 312 // printOffers prints a tabular summary of the offers. 313 func printOffers(tw *ansiterm.TabWriter, offers map[string]offerStatus) error { 314 if len(offers) == 0 { 315 return nil 316 } 317 w := startSection(tw, false, "Offer", "Application", "Charm", "Rev", "Connected", "Endpoint", "Interface", "Role") 318 for _, offerName := range naturalsort.Sort(stringKeysFromMap(offers)) { 319 offer := offers[offerName] 320 // Sort endpoints alphabetically. 321 endpoints := []string{} 322 for endpoint := range offer.Endpoints { 323 endpoints = append(endpoints, endpoint) 324 } 325 sort.Strings(endpoints) 326 327 for i, endpointName := range endpoints { 328 329 endpoint := offer.Endpoints[endpointName] 330 if i == 0 { 331 // As there is some information about offer and its endpoints, 332 // only display offer information once when the first endpoint is displayed. 333 curl, err := charm.ParseURL(offer.CharmURL) 334 if err != nil { 335 return errors.Trace(err) 336 } 337 w.Println(offerName, offer.ApplicationName, curl.Name, fmt.Sprint(curl.Revision), 338 fmt.Sprintf("%v/%v", offer.ActiveConnectedCount, offer.TotalConnectedCount), 339 endpointName, endpoint.Interface, endpoint.Role) 340 continue 341 } 342 // Subsequent lines only need to display endpoint information. 343 // This will display less noise. 344 w.Println("", "", "", "", endpointName, endpoint.Interface, endpoint.Role) 345 } 346 } 347 endSection(tw) 348 return nil 349 } 350 351 func fromMeterStatusColor(msColor string) *ansiterm.Context { 352 switch msColor { 353 case "green": 354 return output.GoodHighlight 355 case "amber": 356 return output.WarningHighlight 357 case "red": 358 return output.ErrorHighlight 359 } 360 return nil 361 } 362 363 func getModelMessage(model modelStatus) string { 364 // Select the most important message about the model (if any). 365 switch { 366 case model.Status.Message != "": 367 return model.Status.Message 368 case model.AvailableVersion != "": 369 return "upgrade available: " + model.AvailableVersion 370 default: 371 return "" 372 } 373 } 374 375 func printMachines(tw *ansiterm.TabWriter, standAlone bool, machines map[string]machineStatus) { 376 w := startSection(tw, standAlone, "Machine", "State", "DNS", "Inst id", "Series", "AZ", "Message") 377 for _, name := range naturalsort.Sort(stringKeysFromMap(machines)) { 378 printMachine(w, machines[name]) 379 } 380 endSection(tw) 381 } 382 383 func printMachine(w output.Wrapper, m machineStatus) { 384 // We want to display availability zone so extract from hardware info". 385 hw, err := instance.ParseHardware(m.Hardware) 386 if err != nil { 387 logger.Warningf("invalid hardware info %s for machine %v", m.Hardware, m) 388 } 389 az := "" 390 if hw.AvailabilityZone != nil { 391 az = *hw.AvailabilityZone 392 } 393 394 w.Print(m.Id) 395 w.PrintStatus(m.JujuStatus.Current) 396 w.Println(m.DNSName, m.machineName(), m.Series, az, m.MachineStatus.Message) 397 for _, name := range naturalsort.Sort(stringKeysFromMap(m.Containers)) { 398 printMachine(w, m.Containers[name]) 399 } 400 } 401 402 // FormatMachineTabular writes a tabular summary of machine 403 func FormatMachineTabular(writer io.Writer, forceColor bool, value interface{}) error { 404 fs, valueConverted := value.(formattedMachineStatus) 405 if !valueConverted { 406 return errors.Errorf("expected value of type %T, got %T", fs, value) 407 } 408 tw := output.TabWriter(writer) 409 if forceColor { 410 tw.SetColorCapable(forceColor) 411 } 412 printMachines(tw, true, fs.Machines) 413 return nil 414 } 415 416 // agentDoing returns what hook or action, if any, 417 // the agent is currently executing. 418 // The hook name or action is extracted from the agent message. 419 func agentDoing(agentStatus statusInfoContents) string { 420 if agentStatus.Current != status.Executing { 421 return "" 422 } 423 // First see if we can determine a hook name. 424 var hookNames []string 425 for _, h := range hooks.UnitHooks() { 426 hookNames = append(hookNames, string(h)) 427 } 428 for _, h := range hooks.RelationHooks() { 429 hookNames = append(hookNames, string(h)) 430 } 431 hookExp := regexp.MustCompile(fmt.Sprintf(`running (?P<hook>%s?) hook`, strings.Join(hookNames, "|"))) 432 match := hookExp.FindStringSubmatch(agentStatus.Message) 433 if len(match) > 0 { 434 return match[1] 435 } 436 // Now try for an action name. 437 actionExp := regexp.MustCompile(`running action (?P<action>.*)`) 438 match = actionExp.FindStringSubmatch(agentStatus.Message) 439 if len(match) > 0 { 440 return match[1] 441 } 442 return "" 443 }