github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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/utils" 16 "github.com/juju/utils/set" 17 "gopkg.in/juju/charm.v6-unstable/hooks" 18 19 "github.com/juju/juju/cmd/output" 20 "github.com/juju/juju/instance" 21 "github.com/juju/juju/status" 22 ) 23 24 type statusRelation struct { 25 application1 string 26 application2 string 27 relation string 28 subordinate bool 29 } 30 31 func (s *statusRelation) relationType() string { 32 if s.subordinate { 33 return "subordinate" 34 } else if s.application1 == s.application2 { 35 return "peer" 36 } 37 return "regular" 38 } 39 40 type relationFormatter struct { 41 relationIndex set.Strings 42 relations map[string]*statusRelation 43 } 44 45 func newRelationFormatter() *relationFormatter { 46 return &relationFormatter{ 47 relationIndex: set.NewStrings(), 48 relations: make(map[string]*statusRelation), 49 } 50 } 51 52 func (r *relationFormatter) len() int { 53 return r.relationIndex.Size() 54 } 55 56 func (r *relationFormatter) add(rel1, rel2, relation string, is2SubOf1 bool) { 57 rel := []string{rel1, rel2} 58 if !is2SubOf1 { 59 sort.Sort(sort.StringSlice(rel)) 60 } 61 k := strings.Join(rel, "\t") 62 r.relations[k] = &statusRelation{ 63 application1: rel[0], 64 application2: rel[1], 65 relation: relation, 66 subordinate: is2SubOf1, 67 } 68 r.relationIndex.Add(k) 69 } 70 71 func (r *relationFormatter) sorted() []string { 72 return r.relationIndex.SortedValues() 73 } 74 75 func (r *relationFormatter) get(k string) *statusRelation { 76 return r.relations[k] 77 } 78 79 // FormatTabular writes a tabular summary of machines, applications, and 80 // units. Any subordinate items are indented by two spaces beneath 81 // their superior. 82 func FormatTabular(writer io.Writer, forceColor bool, value interface{}) error { 83 const maxVersionWidth = 15 84 const ellipsis = "..." 85 const truncatedWidth = maxVersionWidth - len(ellipsis) 86 87 fs, valueConverted := value.(formattedStatus) 88 if !valueConverted { 89 return errors.Errorf("expected value of type %T, got %T", fs, value) 90 } 91 // To format things into columns. 92 tw := output.TabWriter(writer) 93 if forceColor { 94 tw.SetColorCapable(forceColor) 95 } 96 w := output.Wrapper{tw} 97 p := w.Println 98 outputHeaders := func(values ...interface{}) { 99 p() 100 p(values...) 101 } 102 103 cloudRegion := fs.Model.Cloud 104 if fs.Model.CloudRegion != "" { 105 cloudRegion += "/" + fs.Model.CloudRegion 106 } 107 108 header := []interface{}{"MODEL", "CONTROLLER", "CLOUD/REGION", "VERSION"} 109 values := []interface{}{fs.Model.Name, fs.Model.Controller, cloudRegion, fs.Model.Version} 110 message := getModelMessage(fs.Model) 111 if message != "" { 112 header = append(header, "NOTES") 113 values = append(values, message) 114 } 115 116 // The first set of headers don't use outputHeaders because it adds the blank line. 117 p(header...) 118 p(values...) 119 120 units := make(map[string]unitStatus) 121 metering := false 122 relations := newRelationFormatter() 123 outputHeaders("APP", "VERSION", "STATUS", "SCALE", "CHARM", "STORE", "REV", "OS", "NOTES") 124 tw.SetColumnAlignRight(3) 125 tw.SetColumnAlignRight(6) 126 for _, appName := range utils.SortStringsNaturally(stringKeysFromMap(fs.Applications)) { 127 app := fs.Applications[appName] 128 version := app.Version 129 // Don't let a long version push out the version column. 130 if len(version) > maxVersionWidth { 131 version = version[:truncatedWidth] + ellipsis 132 } 133 // Notes may well contain other things later. 134 notes := "" 135 if app.Exposed { 136 notes = "exposed" 137 } 138 w.Print(appName, version) 139 w.PrintStatus(app.StatusInfo.Current) 140 scale, warn := fs.applicationScale(appName) 141 if warn { 142 w.PrintColor(output.WarningHighlight, scale) 143 } else { 144 w.Print(scale) 145 } 146 p(app.CharmName, 147 app.CharmOrigin, 148 app.CharmRev, 149 app.OS, 150 notes) 151 152 for un, u := range app.Units { 153 units[un] = u 154 if u.MeterStatus != nil { 155 metering = true 156 } 157 } 158 // Ensure that we pick a consistent name for peer relations. 159 sortedRelTypes := make([]string, 0, len(app.Relations)) 160 for relType := range app.Relations { 161 sortedRelTypes = append(sortedRelTypes, relType) 162 } 163 sort.Strings(sortedRelTypes) 164 165 subs := set.NewStrings(app.SubordinateTo...) 166 for _, relType := range sortedRelTypes { 167 for _, related := range app.Relations[relType] { 168 relations.add(related, appName, relType, subs.Contains(related)) 169 } 170 } 171 172 } 173 174 pUnit := func(name string, u unitStatus, level int) { 175 message := u.WorkloadStatusInfo.Message 176 agentDoing := agentDoing(u.JujuStatusInfo) 177 if agentDoing != "" { 178 message = fmt.Sprintf("(%s) %s", agentDoing, message) 179 } 180 if u.Leader { 181 name += "*" 182 } 183 w.Print(indent("", level*2, name)) 184 w.PrintStatus(u.WorkloadStatusInfo.Current) 185 w.PrintStatus(u.JujuStatusInfo.Current) 186 p( 187 u.Machine, 188 u.PublicAddress, 189 strings.Join(u.OpenedPorts, ","), 190 message, 191 ) 192 } 193 194 outputHeaders("UNIT", "WORKLOAD", "AGENT", "MACHINE", "PUBLIC-ADDRESS", "PORTS", "MESSAGE") 195 for _, name := range utils.SortStringsNaturally(stringKeysFromMap(units)) { 196 u := units[name] 197 pUnit(name, u, 0) 198 const indentationLevel = 1 199 recurseUnits(u, indentationLevel, pUnit) 200 } 201 202 if metering { 203 outputHeaders("METER", "STATUS", "MESSAGE") 204 for _, name := range utils.SortStringsNaturally(stringKeysFromMap(units)) { 205 u := units[name] 206 if u.MeterStatus != nil { 207 p(name, u.MeterStatus.Color, u.MeterStatus.Message) 208 } 209 } 210 } 211 212 p() 213 printMachines(tw, fs.Machines) 214 215 if relations.len() > 0 { 216 outputHeaders("RELATION", "PROVIDES", "CONSUMES", "TYPE") 217 for _, k := range relations.sorted() { 218 r := relations.get(k) 219 if r != nil { 220 p(r.relation, r.application1, r.application2, r.relationType()) 221 } 222 } 223 } 224 225 tw.Flush() 226 return nil 227 } 228 229 func getModelMessage(model modelStatus) string { 230 // Select the most important message about the model (if any). 231 switch { 232 case model.Migration != "": 233 return "migrating: " + model.Migration 234 case model.AvailableVersion != "": 235 return "upgrade available: " + model.AvailableVersion 236 default: 237 return "" 238 } 239 } 240 241 func printMachines(tw *ansiterm.TabWriter, machines map[string]machineStatus) { 242 w := output.Wrapper{tw} 243 w.Println("MACHINE", "STATE", "DNS", "INS-ID", "SERIES", "AZ") 244 for _, name := range utils.SortStringsNaturally(stringKeysFromMap(machines)) { 245 printMachine(w, machines[name]) 246 } 247 } 248 249 func printMachine(w output.Wrapper, m machineStatus) { 250 // We want to display availability zone so extract from hardware info". 251 hw, err := instance.ParseHardware(m.Hardware) 252 if err != nil { 253 logger.Warningf("invalid hardware info %s for machine %v", m.Hardware, m) 254 } 255 az := "" 256 if hw.AvailabilityZone != nil { 257 az = *hw.AvailabilityZone 258 } 259 w.Print(m.Id) 260 w.PrintStatus(m.JujuStatus.Current) 261 w.Println(m.DNSName, m.InstanceId, m.Series, az) 262 for _, name := range utils.SortStringsNaturally(stringKeysFromMap(m.Containers)) { 263 printMachine(w, m.Containers[name]) 264 } 265 } 266 267 // FormatMachineTabular writes a tabular summary of machine 268 func FormatMachineTabular(writer io.Writer, forceColor bool, value interface{}) error { 269 fs, valueConverted := value.(formattedMachineStatus) 270 if !valueConverted { 271 return errors.Errorf("expected value of type %T, got %T", fs, value) 272 } 273 tw := output.TabWriter(writer) 274 if forceColor { 275 tw.SetColorCapable(forceColor) 276 } 277 printMachines(tw, fs.Machines) 278 tw.Flush() 279 280 return nil 281 } 282 283 // agentDoing returns what hook or action, if any, 284 // the agent is currently executing. 285 // The hook name or action is extracted from the agent message. 286 func agentDoing(agentStatus statusInfoContents) string { 287 if agentStatus.Current != status.Executing { 288 return "" 289 } 290 // First see if we can determine a hook name. 291 var hookNames []string 292 for _, h := range hooks.UnitHooks() { 293 hookNames = append(hookNames, string(h)) 294 } 295 for _, h := range hooks.RelationHooks() { 296 hookNames = append(hookNames, string(h)) 297 } 298 hookExp := regexp.MustCompile(fmt.Sprintf(`running (?P<hook>%s?) hook`, strings.Join(hookNames, "|"))) 299 match := hookExp.FindStringSubmatch(agentStatus.Message) 300 if len(match) > 0 { 301 return match[1] 302 } 303 // Now try for an action name. 304 actionExp := regexp.MustCompile(`running action (?P<action>.*)`) 305 match = actionExp.FindStringSubmatch(agentStatus.Message) 306 if len(match) > 0 { 307 return match[1] 308 } 309 return "" 310 }