github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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 "bytes" 8 "fmt" 9 "regexp" 10 "sort" 11 "strings" 12 "text/tabwriter" 13 14 "github.com/juju/errors" 15 "github.com/juju/utils/set" 16 "gopkg.in/juju/charm.v6-unstable/hooks" 17 18 "github.com/juju/juju/cmd/juju/common" 19 "github.com/juju/juju/instance" 20 "github.com/juju/juju/status" 21 ) 22 23 type statusRelation struct { 24 service1 string 25 service2 string 26 relation string 27 subordinate bool 28 } 29 30 func (s *statusRelation) relationType() string { 31 if s.subordinate { 32 return "subordinate" 33 } else if s.service1 == s.service2 { 34 return "peer" 35 } 36 return "regular" 37 } 38 39 type relationFormatter struct { 40 relationIndex set.Strings 41 relations map[string]*statusRelation 42 } 43 44 func newRelationFormatter() *relationFormatter { 45 return &relationFormatter{ 46 relationIndex: set.NewStrings(), 47 relations: make(map[string]*statusRelation), 48 } 49 } 50 51 func (r *relationFormatter) len() int { 52 return r.relationIndex.Size() 53 } 54 55 func (r *relationFormatter) add(rel1, rel2, relation string, is2SubOf1 bool) { 56 rel := []string{rel1, rel2} 57 if !is2SubOf1 { 58 sort.Sort(sort.StringSlice(rel)) 59 } 60 k := strings.Join(rel, "\t") 61 r.relations[k] = &statusRelation{ 62 service1: rel[0], 63 service2: rel[1], 64 relation: relation, 65 subordinate: is2SubOf1, 66 } 67 r.relationIndex.Add(k) 68 } 69 70 func (r *relationFormatter) sorted() []string { 71 return r.relationIndex.SortedValues() 72 } 73 74 func (r *relationFormatter) get(k string) *statusRelation { 75 return r.relations[k] 76 } 77 78 // FormatTabular returns a tabular summary of machines, services, and 79 // units. Any subordinate items are indented by two spaces beneath 80 // their superior. 81 func FormatTabular(value interface{}) ([]byte, error) { 82 fs, valueConverted := value.(formattedStatus) 83 if !valueConverted { 84 return nil, errors.Errorf("expected value of type %T, got %T", fs, value) 85 } 86 var out bytes.Buffer 87 // To format things into columns. 88 tw := tabwriter.NewWriter(&out, 0, 1, 1, ' ', 0) 89 p := func(values ...interface{}) { 90 for _, v := range values { 91 fmt.Fprintf(tw, "%s\t", v) 92 } 93 fmt.Fprintln(tw) 94 } 95 96 if envStatus := fs.ModelStatus; envStatus != nil { 97 p("[Model]") 98 if envStatus.AvailableVersion != "" { 99 p("UPGRADE-AVAILABLE") 100 p(envStatus.AvailableVersion) 101 } 102 p() 103 tw.Flush() 104 } 105 106 units := make(map[string]unitStatus) 107 metering := false 108 relations := newRelationFormatter() 109 p("[Services]") 110 p("NAME\tSTATUS\tEXPOSED\tCHARM") 111 for _, svcName := range common.SortStringsNaturally(stringKeysFromMap(fs.Services)) { 112 svc := fs.Services[svcName] 113 for un, u := range svc.Units { 114 units[un] = u 115 if u.MeterStatus != nil { 116 metering = true 117 } 118 } 119 120 subs := set.NewStrings(svc.SubordinateTo...) 121 p(svcName, svc.StatusInfo.Current, fmt.Sprintf("%t", svc.Exposed), svc.Charm) 122 for relType, relatedUnits := range svc.Relations { 123 for _, related := range relatedUnits { 124 relations.add(related, svcName, relType, subs.Contains(related)) 125 } 126 } 127 128 } 129 if relations.len() > 0 { 130 p() 131 p("[Relations]") 132 p("SERVICE1\tSERVICE2\tRELATION\tTYPE") 133 for _, k := range relations.sorted() { 134 r := relations.get(k) 135 if r != nil { 136 p(r.service1, r.service2, r.relation, r.relationType()) 137 } 138 } 139 } 140 tw.Flush() 141 142 pUnit := func(name string, u unitStatus, level int) { 143 message := u.WorkloadStatusInfo.Message 144 agentDoing := agentDoing(u.JujuStatusInfo) 145 if agentDoing != "" { 146 message = fmt.Sprintf("(%s) %s", agentDoing, message) 147 } 148 p( 149 indent("", level*2, name), 150 u.WorkloadStatusInfo.Current, 151 u.JujuStatusInfo.Current, 152 u.JujuStatusInfo.Version, 153 u.Machine, 154 strings.Join(u.OpenedPorts, ","), 155 u.PublicAddress, 156 message, 157 ) 158 } 159 160 header := []string{"ID", "WORKLOAD-STATUS", "JUJU-STATUS", "VERSION", "MACHINE", "PORTS", "PUBLIC-ADDRESS", "MESSAGE"} 161 162 p("\n[Units]") 163 p(strings.Join(header, "\t")) 164 for _, name := range common.SortStringsNaturally(stringKeysFromMap(units)) { 165 u := units[name] 166 pUnit(name, u, 0) 167 const indentationLevel = 1 168 recurseUnits(u, indentationLevel, pUnit) 169 } 170 tw.Flush() 171 172 if metering { 173 p("\n[Metering]") 174 p("ID\tSTATUS\tMESSAGE") 175 for _, name := range common.SortStringsNaturally(stringKeysFromMap(units)) { 176 u := units[name] 177 if u.MeterStatus != nil { 178 p(name, u.MeterStatus.Color, u.MeterStatus.Message) 179 } 180 } 181 } 182 183 p("\n[Machines]") 184 p("ID\tSTATE\tDNS\tINS-ID\tSERIES\tAZ") 185 for _, name := range common.SortStringsNaturally(stringKeysFromMap(fs.Machines)) { 186 m := fs.Machines[name] 187 // We want to display availability zone so extract from hardware info". 188 hw, err := instance.ParseHardware(m.Hardware) 189 if err != nil { 190 logger.Warningf("invalid hardware info %s for machine %v", m.Hardware, m) 191 } 192 az := "" 193 if hw.AvailabilityZone != nil { 194 az = *hw.AvailabilityZone 195 } 196 p(m.Id, m.JujuStatus.Current, m.DNSName, m.InstanceId, m.Series, az) 197 } 198 tw.Flush() 199 return out.Bytes(), nil 200 } 201 202 // FormatMachineTabular returns a tabular summary of machine 203 func FormatMachineTabular(value interface{}) ([]byte, error) { 204 fs, valueConverted := value.(formattedMachineStatus) 205 if !valueConverted { 206 return nil, errors.Errorf("expected value of type %T, got %T", fs, value) 207 } 208 var out bytes.Buffer 209 // To format things into columns. 210 tw := tabwriter.NewWriter(&out, 0, 1, 1, ' ', 0) 211 p := func(values ...interface{}) { 212 for _, v := range values { 213 fmt.Fprintf(tw, "%s\t", v) 214 } 215 fmt.Fprintln(tw) 216 } 217 218 p("\n[Machines]") 219 p("ID\tSTATE\tDNS\tINS-ID\tSERIES\tAZ") 220 for _, name := range common.SortStringsNaturally(stringKeysFromMap(fs.Machines)) { 221 m := fs.Machines[name] 222 // We want to display availability zone so extract from hardware info". 223 hw, err := instance.ParseHardware(m.Hardware) 224 if err != nil { 225 logger.Warningf("invalid hardware info %s for machine %v", m.Hardware, m) 226 } 227 az := "" 228 if hw.AvailabilityZone != nil { 229 az = *hw.AvailabilityZone 230 } 231 p(m.Id, m.JujuStatus.Current, m.DNSName, m.InstanceId, m.Series, az) 232 } 233 tw.Flush() 234 235 return out.Bytes(), nil 236 } 237 238 // agentDoing returns what hook or action, if any, 239 // the agent is currently executing. 240 // The hook name or action is extracted from the agent message. 241 func agentDoing(agentStatus statusInfoContents) string { 242 if agentStatus.Current != status.StatusExecuting { 243 return "" 244 } 245 // First see if we can determine a hook name. 246 var hookNames []string 247 for _, h := range hooks.UnitHooks() { 248 hookNames = append(hookNames, string(h)) 249 } 250 for _, h := range hooks.RelationHooks() { 251 hookNames = append(hookNames, string(h)) 252 } 253 hookExp := regexp.MustCompile(fmt.Sprintf(`running (?P<hook>%s?) hook`, strings.Join(hookNames, "|"))) 254 match := hookExp.FindStringSubmatch(agentStatus.Message) 255 if len(match) > 0 { 256 return match[1] 257 } 258 // Now try for an action name. 259 actionExp := regexp.MustCompile(`running action (?P<action>.*)`) 260 match = actionExp.FindStringSubmatch(agentStatus.Message) 261 if len(match) > 0 { 262 return match[1] 263 } 264 return "" 265 }