github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/cmd/juju/commands/status_formatters.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package commands 5 6 import ( 7 "bytes" 8 "fmt" 9 "net" 10 "reflect" 11 "regexp" 12 "sort" 13 "strings" 14 "text/tabwriter" 15 16 "github.com/juju/errors" 17 "github.com/juju/utils/set" 18 "gopkg.in/juju/charm.v5/hooks" 19 20 "github.com/juju/juju/apiserver/params" 21 ) 22 23 // FormatOneline returns a brief list of units and their subordinates. 24 // Subordinates will be indented 2 spaces and listed under their 25 // superiors. 26 func FormatOneline(value interface{}) ([]byte, error) { 27 fs, valueConverted := value.(formattedStatus) 28 if !valueConverted { 29 return nil, errors.Errorf("expected value of type %T, got %T", fs, value) 30 } 31 var out bytes.Buffer 32 33 pprint := func(uName string, u unitStatus, level int) { 34 var fmtPorts string 35 if len(u.OpenedPorts) > 0 { 36 fmtPorts = fmt.Sprintf(" %s", strings.Join(u.OpenedPorts, ", ")) 37 } 38 fmt.Fprintf(&out, indent("\n", level*2, "- %s: %s (%v)%v"), 39 uName, 40 u.PublicAddress, 41 u.AgentState, 42 fmtPorts, 43 ) 44 } 45 46 for _, svcName := range sortStringsNaturally(stringKeysFromMap(fs.Services)) { 47 svc := fs.Services[svcName] 48 for _, uName := range sortStringsNaturally(stringKeysFromMap(svc.Units)) { 49 unit := svc.Units[uName] 50 pprint(uName, unit, 0) 51 recurseUnits(unit, 1, pprint) 52 } 53 } 54 55 return out.Bytes(), nil 56 } 57 58 // agentDoing returns what hook or action, if any, 59 // the agent is currently executing. 60 // The hook name or action is extracted from the agent message. 61 func agentDoing(status statusInfoContents) string { 62 if status.Current != params.StatusExecuting { 63 return "" 64 } 65 // First see if we can determine a hook name. 66 var hookNames []string 67 for _, h := range hooks.UnitHooks() { 68 hookNames = append(hookNames, string(h)) 69 } 70 for _, h := range hooks.RelationHooks() { 71 hookNames = append(hookNames, string(h)) 72 } 73 hookExp := regexp.MustCompile(fmt.Sprintf(`running (?P<hook>%s?) hook`, strings.Join(hookNames, "|"))) 74 match := hookExp.FindStringSubmatch(status.Message) 75 if len(match) > 0 { 76 return match[1] 77 } 78 // Now try for an action name. 79 actionExp := regexp.MustCompile(`running action (?P<action>.*)`) 80 match = actionExp.FindStringSubmatch(status.Message) 81 if len(match) > 0 { 82 return match[1] 83 } 84 return "" 85 } 86 87 // FormatTabular returns a tabular summary of machines, services, and 88 // units. Any subordinate items are indented by two spaces beneath 89 // their superior. 90 func FormatTabular(value interface{}) ([]byte, error) { 91 fs, valueConverted := value.(formattedStatus) 92 if !valueConverted { 93 return nil, errors.Errorf("expected value of type %T, got %T", fs, value) 94 } 95 var out bytes.Buffer 96 // To format things into columns. 97 tw := tabwriter.NewWriter(&out, 0, 1, 1, ' ', 0) 98 p := func(values ...interface{}) { 99 for _, v := range values { 100 fmt.Fprintf(tw, "%s\t", v) 101 } 102 fmt.Fprintln(tw) 103 } 104 105 units := make(map[string]unitStatus) 106 p("[Services]") 107 p("NAME\tSTATUS\tEXPOSED\tCHARM") 108 for _, svcName := range sortStringsNaturally(stringKeysFromMap(fs.Services)) { 109 svc := fs.Services[svcName] 110 for un, u := range svc.Units { 111 units[un] = u 112 } 113 p(svcName, svc.StatusInfo.Current, fmt.Sprintf("%t", svc.Exposed), svc.Charm) 114 } 115 tw.Flush() 116 117 pUnit := func(name string, u unitStatus, level int) { 118 message := u.WorkloadStatusInfo.Message 119 agentDoing := agentDoing(u.AgentStatusInfo) 120 if agentDoing != "" { 121 message = fmt.Sprintf("(%s) %s", agentDoing, message) 122 } 123 p( 124 indent("", level*2, name), 125 u.WorkloadStatusInfo.Current, 126 u.AgentStatusInfo.Current, 127 u.AgentStatusInfo.Version, 128 u.Machine, 129 strings.Join(u.OpenedPorts, ","), 130 u.PublicAddress, 131 message, 132 ) 133 } 134 135 // See if we have new or old data; that determines what data we can display. 136 newStatus := false 137 for _, u := range units { 138 if u.AgentStatusInfo.Current != "" { 139 newStatus = true 140 break 141 } 142 } 143 var header []string 144 if newStatus { 145 header = []string{"ID", "WORKLOAD-STATE", "AGENT-STATE", "VERSION", "MACHINE", "PORTS", "PUBLIC-ADDRESS", "MESSAGE"} 146 } else { 147 header = []string{"ID", "STATE", "VERSION", "MACHINE", "PORTS", "PUBLIC-ADDRESS"} 148 } 149 150 p("\n[Units]") 151 p(strings.Join(header, "\t")) 152 for _, name := range sortStringsNaturally(stringKeysFromMap(units)) { 153 u := units[name] 154 pUnit(name, u, 0) 155 const indentationLevel = 1 156 recurseUnits(u, indentationLevel, pUnit) 157 } 158 tw.Flush() 159 160 p("\n[Machines]") 161 p("ID\tSTATE\tVERSION\tDNS\tINS-ID\tSERIES\tHARDWARE") 162 for _, name := range sortStringsNaturally(stringKeysFromMap(fs.Machines)) { 163 m := fs.Machines[name] 164 p(m.Id, m.AgentState, m.AgentVersion, m.DNSName, m.InstanceId, m.Series, m.Hardware) 165 } 166 tw.Flush() 167 168 return out.Bytes(), nil 169 } 170 171 // FormatSummary returns a summary of the current environment 172 // including the following information: 173 // - Headers: 174 // - All subnets the environment occupies. 175 // - All ports the environment utilizes. 176 // - Sections: 177 // - Machines: Displays total #, and then the # in each state. 178 // - Units: Displays total #, and then # in each state. 179 // - Services: Displays total #, their names, and how many of each 180 // are exposed. 181 func FormatSummary(value interface{}) ([]byte, error) { 182 fs, valueConverted := value.(formattedStatus) 183 if !valueConverted { 184 return nil, errors.Errorf("expected value of type %T, got %T", fs, value) 185 } 186 187 f := newSummaryFormatter() 188 stateToMachine := f.aggregateMachineStates(fs.Machines) 189 svcExposure := f.aggregateServiceAndUnitStates(fs.Services) 190 p := f.delimitValuesWithTabs 191 192 // Print everything out 193 p("Running on subnets:", strings.Join(f.netStrings, ", ")) 194 p("Utilizing ports:", f.portsInColumnsOf(3)) 195 f.tw.Flush() 196 197 // Right align summary information 198 f.tw.Init(&f.out, 0, 2, 1, ' ', tabwriter.AlignRight) 199 p("# MACHINES:", fmt.Sprintf("(%d)", len(fs.Machines))) 200 f.printStateToCount(stateToMachine) 201 p(" ") 202 203 p("# UNITS:", fmt.Sprintf("(%d)", f.numUnits)) 204 f.printStateToCount(f.stateToUnit) 205 p(" ") 206 207 p("# SERVICES:", fmt.Sprintf(" (%d)", len(fs.Services))) 208 for _, svcName := range sortStringsNaturally(stringKeysFromMap(svcExposure)) { 209 s := svcExposure[svcName] 210 p(svcName, fmt.Sprintf("%d/%d\texposed", s[true], s[true]+s[false])) 211 } 212 f.tw.Flush() 213 214 return f.out.Bytes(), nil 215 } 216 217 func newSummaryFormatter() *summaryFormatter { 218 f := &summaryFormatter{ 219 ipAddrs: make([]net.IPNet, 0), 220 netStrings: make([]string, 0), 221 openPorts: set.NewStrings(), 222 stateToUnit: make(map[params.Status]int), 223 } 224 f.tw = tabwriter.NewWriter(&f.out, 0, 1, 1, ' ', 0) 225 return f 226 } 227 228 type summaryFormatter struct { 229 ipAddrs []net.IPNet 230 netStrings []string 231 numUnits int 232 openPorts set.Strings 233 // status -> count 234 stateToUnit map[params.Status]int 235 tw *tabwriter.Writer 236 out bytes.Buffer 237 } 238 239 func (f *summaryFormatter) delimitValuesWithTabs(values ...string) { 240 for _, v := range values { 241 fmt.Fprintf(f.tw, "%s\t", v) 242 } 243 fmt.Fprintln(f.tw) 244 } 245 246 func (f *summaryFormatter) portsInColumnsOf(col int) string { 247 248 var b bytes.Buffer 249 for i, p := range f.openPorts.SortedValues() { 250 if i != 0 && i%col == 0 { 251 fmt.Fprintf(&b, "\n\t") 252 } 253 fmt.Fprintf(&b, "%s, ", p) 254 } 255 // Elide the last delimiter 256 portList := b.String() 257 if len(portList) >= 2 { 258 return portList[:b.Len()-2] 259 } 260 return portList 261 } 262 263 func (f *summaryFormatter) trackUnit(name string, status unitStatus, indentLevel int) { 264 f.resolveAndTrackIp(status.PublicAddress) 265 266 for _, p := range status.OpenedPorts { 267 if p != "" { 268 f.openPorts.Add(p) 269 } 270 } 271 f.numUnits++ 272 f.stateToUnit[status.AgentState]++ 273 } 274 275 func (f *summaryFormatter) printStateToCount(m map[params.Status]int) { 276 for _, status := range sortStringsNaturally(stringKeysFromMap(m)) { 277 numInStatus := m[params.Status(status)] 278 f.delimitValuesWithTabs(status+":", fmt.Sprintf(" %d ", numInStatus)) 279 } 280 } 281 282 func (f *summaryFormatter) trackIp(ip net.IP) { 283 for _, net := range f.ipAddrs { 284 if net.Contains(ip) { 285 return 286 } 287 } 288 289 ipNet := net.IPNet{ip, ip.DefaultMask()} 290 f.ipAddrs = append(f.ipAddrs, ipNet) 291 f.netStrings = append(f.netStrings, ipNet.String()) 292 } 293 294 func (f *summaryFormatter) resolveAndTrackIp(publicDns string) { 295 // TODO(katco-): We may be able to utilize upcoming work which will expose these addresses outright. 296 ip, err := net.ResolveIPAddr("ip4", publicDns) 297 if err != nil { 298 logger.Warningf( 299 "unable to resolve %s to an IP address. Status may be incorrect: %v", 300 publicDns, 301 err, 302 ) 303 return 304 } 305 f.trackIp(ip.IP) 306 } 307 308 func (f *summaryFormatter) aggregateMachineStates(machines map[string]machineStatus) map[params.Status]int { 309 stateToMachine := make(map[params.Status]int) 310 for _, name := range sortStringsNaturally(stringKeysFromMap(machines)) { 311 m := machines[name] 312 f.resolveAndTrackIp(m.DNSName) 313 314 if agentState := m.AgentState; agentState == "" { 315 agentState = params.StatusPending 316 } else { 317 stateToMachine[agentState]++ 318 } 319 } 320 return stateToMachine 321 } 322 323 func (f *summaryFormatter) aggregateServiceAndUnitStates(services map[string]serviceStatus) map[string]map[bool]int { 324 svcExposure := make(map[string]map[bool]int) 325 for _, name := range sortStringsNaturally(stringKeysFromMap(services)) { 326 s := services[name] 327 // Grab unit states 328 for _, un := range sortStringsNaturally(stringKeysFromMap(s.Units)) { 329 u := s.Units[un] 330 f.trackUnit(un, u, 0) 331 recurseUnits(u, 1, f.trackUnit) 332 } 333 334 if _, ok := svcExposure[name]; !ok { 335 svcExposure[name] = make(map[bool]int) 336 } 337 338 svcExposure[name][s.Exposed]++ 339 } 340 return svcExposure 341 } 342 343 // sortStringsNaturally is syntactic sugar so we can do sorts in one line. 344 func sortStringsNaturally(s []string) []string { 345 sort.Sort(naturally(s)) 346 return s 347 } 348 349 // stringKeysFromMap takes a map with keys which are strings and returns 350 // only the keys. 351 func stringKeysFromMap(m interface{}) (keys []string) { 352 for _, k := range reflect.ValueOf(m).MapKeys() { 353 keys = append(keys, k.String()) 354 } 355 return 356 } 357 358 // recurseUnits calls the given recurseMap function on the given unit 359 // and its subordinates (recursively defined on the given unit). 360 func recurseUnits(u unitStatus, il int, recurseMap func(string, unitStatus, int)) { 361 if len(u.Subordinates) == 0 { 362 return 363 } 364 for _, uName := range sortStringsNaturally(stringKeysFromMap(u.Subordinates)) { 365 unit := u.Subordinates[uName] 366 recurseMap(uName, unit, il) 367 recurseUnits(unit, il+1, recurseMap) 368 } 369 } 370 371 // indent prepends a format string with the given number of spaces. 372 func indent(prepend string, level int, append string) string { 373 return fmt.Sprintf("%s%*s%s", prepend, level, "", append) 374 }