launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/state/statecmd/status.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package statecmd 5 6 import ( 7 "fmt" 8 "path" 9 "regexp" 10 "strings" 11 12 errgo "launchpad.net/errgo/errors" 13 "launchpad.net/juju-core/charm" 14 "launchpad.net/juju-core/errors" 15 "launchpad.net/juju-core/instance" 16 "launchpad.net/juju-core/juju" 17 "launchpad.net/juju-core/state" 18 "launchpad.net/juju-core/state/api" 19 "launchpad.net/juju-core/state/api/params" 20 "launchpad.net/juju-core/tools" 21 "launchpad.net/juju-core/utils/set" 22 ) 23 24 func Status(conn *juju.Conn, patterns []string) (*api.Status, error) { 25 var nilStatus api.Status 26 var context statusContext 27 unitMatcher, err := NewUnitMatcher(patterns) 28 if err != nil { 29 return &nilStatus, mask(err) 30 } 31 if context.services, 32 context.units, context.latestCharms, err = fetchAllServicesAndUnits(conn.State, unitMatcher); err != nil { 33 return &nilStatus, mask(err) 34 } 35 36 // Filter machines by units in scope. 37 var machineIds *set.Strings 38 if !unitMatcher.matchesAny() { 39 machineIds, err = fetchUnitMachineIds(context.units) 40 if err != nil { 41 return &nilStatus, mask(err) 42 } 43 } 44 if context.machines, err = fetchMachines(conn.State, machineIds); err != nil { 45 return &nilStatus, mask(err) 46 } 47 48 return &api.Status{ 49 EnvironmentName: conn.Environ.Name(), 50 Machines: context.processMachines(), 51 Services: context.processServices(), 52 }, nil 53 } 54 55 type statusContext struct { 56 machines map[string][]*state.Machine 57 services map[string]*state.Service 58 units map[string]map[string]*state.Unit 59 latestCharms map[charm.URL]string 60 } 61 62 type unitMatcher struct { 63 patterns []string 64 } 65 66 // matchesAny returns true if the unitMatcher will 67 // match any unit, regardless of its attributes. 68 func (m unitMatcher) matchesAny() bool { 69 return len(m.patterns) == 0 70 } 71 72 // matchUnit attempts to match a state.Unit to one of 73 // a set of patterns, taking into account subordinate 74 // relationships. 75 func (m unitMatcher) matchUnit(u *state.Unit) bool { 76 if m.matchesAny() { 77 return true 78 } 79 80 // Keep the unit if: 81 // (a) its name matches a pattern, or 82 // (b) it's a principal and one of its subordinates matches, or 83 // (c) it's a subordinate and its principal matches. 84 // 85 // Note: do *not* include a second subordinate if the principal is 86 // only matched on account of a first subordinate matching. 87 if m.matchString(u.Name()) { 88 return true 89 } 90 if u.IsPrincipal() { 91 for _, s := range u.SubordinateNames() { 92 if m.matchString(s) { 93 return true 94 } 95 } 96 return false 97 } 98 principal, valid := u.PrincipalName() 99 if !valid { 100 panic("PrincipalName failed for subordinate unit") 101 } 102 return m.matchString(principal) 103 } 104 105 // matchString matches a string to one of the patterns in 106 // the unit matcher, returning an error if a pattern with 107 // invalid syntax is encountered. 108 func (m unitMatcher) matchString(s string) bool { 109 for _, pattern := range m.patterns { 110 ok, err := path.Match(pattern, s) 111 if err != nil { 112 // We validate patterns, so should never get here. 113 panic(errgo.Newf("pattern syntax error in %q", pattern)) 114 } else if ok { 115 return true 116 } 117 } 118 return false 119 } 120 121 // validPattern must match the parts of a unit or service name 122 // pattern either side of the '/' for it to be valid. 123 var validPattern = regexp.MustCompile("^[a-z0-9-*]+$") 124 125 // NewUnitMatcher returns a unitMatcher that matches units 126 // with one of the specified patterns, or all units if no 127 // patterns are specified. 128 // 129 // An error will be returned if any of the specified patterns 130 // is invalid. Patterns are valid if they contain only 131 // alpha-numeric characters, hyphens, or asterisks (and one 132 // optional '/' to separate service/unit). 133 func NewUnitMatcher(patterns []string) (unitMatcher, error) { 134 for i, pattern := range patterns { 135 fields := strings.Split(pattern, "/") 136 if len(fields) > 2 { 137 return unitMatcher{}, errgo.Newf("pattern %q contains too many '/' characters", pattern) 138 } 139 for _, f := range fields { 140 if !validPattern.MatchString(f) { 141 return unitMatcher{}, errgo.Newf("pattern %q contains invalid characters", pattern) 142 } 143 } 144 if len(fields) == 1 { 145 patterns[i] += "/*" 146 } 147 } 148 return unitMatcher{patterns}, nil 149 } 150 151 // fetchMachines returns a map from top level machine id to machines, where machines[0] is the host 152 // machine and machines[1..n] are any containers (including nested ones). 153 // 154 // If machineIds is non-nil, only machines whose IDs are in the set are returned. 155 func fetchMachines(st *state.State, machineIds *set.Strings) (map[string][]*state.Machine, error) { 156 v := make(map[string][]*state.Machine) 157 machines, err := st.AllMachines() 158 if err != nil { 159 return nil, mask(err) 160 } 161 162 // AllMachines gives us machines sorted by id. 163 for _, m := range machines { 164 if machineIds != nil && !machineIds.Contains(m.Id()) { 165 continue 166 } 167 parentId, ok := m.ParentId() 168 if !ok { 169 // Only top level host machines go directly into the machine map. 170 v[m.Id()] = []*state.Machine{m} 171 } else { 172 topParentId := state.TopParentId(m.Id()) 173 machines, ok := v[topParentId] 174 if !ok { 175 panic(errgo.Newf("unexpected machine id %q", parentId)) 176 } 177 machines = append(machines, m) 178 v[topParentId] = machines 179 } 180 } 181 return v, nil 182 } 183 184 // fetchAllServicesAndUnits returns a map from service name to service, 185 // a map from service name to unit name to unit, and a map from base charm URL to latest URL. 186 func fetchAllServicesAndUnits( 187 st *state.State, unitMatcher unitMatcher) ( 188 map[string]*state.Service, map[string]map[string]*state.Unit, map[charm.URL]string, error) { 189 190 svcMap := make(map[string]*state.Service) 191 unitMap := make(map[string]map[string]*state.Unit) 192 latestCharms := make(map[charm.URL]string) 193 services, err := st.AllServices() 194 if err != nil { 195 return nil, nil, nil, mask(err) 196 } 197 for _, s := range services { 198 units, err := s.AllUnits() 199 if err != nil { 200 return nil, nil, nil, mask(err) 201 } 202 svcUnitMap := make(map[string]*state.Unit) 203 for _, u := range units { 204 if !unitMatcher.matchUnit(u) { 205 continue 206 } 207 svcUnitMap[u.Name()] = u 208 } 209 if unitMatcher.matchesAny() || len(svcUnitMap) > 0 { 210 unitMap[s.Name()] = svcUnitMap 211 svcMap[s.Name()] = s 212 // Record the base URL for the service's charm so that 213 // the latest store revision can be looked up. 214 charmURL, _ := s.CharmURL() 215 if charmURL.Schema == "cs" { 216 latestCharms[*charmURL.WithRevision(-1)] = "" 217 } 218 } 219 } 220 for baseURL, _ := range latestCharms { 221 ch, err := st.LatestPlaceholderCharm(&baseURL) 222 if errors.IsNotFoundError(err) { 223 continue 224 } 225 if err != nil { 226 return nil, nil, nil, mask(err) 227 } 228 latestCharms[baseURL] = ch.String() 229 } 230 return svcMap, unitMap, latestCharms, nil 231 } 232 233 // fetchUnitMachineIds returns a set of IDs for machines that 234 // the specified units reside on, and those machines' ancestors. 235 func fetchUnitMachineIds(units map[string]map[string]*state.Unit) (*set.Strings, error) { 236 machineIds := new(set.Strings) 237 for _, svcUnitMap := range units { 238 for _, unit := range svcUnitMap { 239 if !unit.IsPrincipal() { 240 continue 241 } 242 mid, err := unit.AssignedMachineId() 243 if err != nil { 244 return nil, mask(err) 245 } 246 for mid != "" { 247 machineIds.Add(mid) 248 mid = state.ParentId(mid) 249 } 250 } 251 } 252 return machineIds, nil 253 } 254 255 func (context *statusContext) processMachines() map[string]api.MachineStatus { 256 machinesMap := make(map[string]api.MachineStatus) 257 for id, machines := range context.machines { 258 hostStatus := context.makeMachineStatus(machines[0]) 259 context.processMachine(machines, &hostStatus, 0) 260 machinesMap[id] = hostStatus 261 } 262 return machinesMap 263 } 264 265 func (context *statusContext) processMachine(machines []*state.Machine, host *api.MachineStatus, startIndex int) (nextIndex int) { 266 nextIndex = startIndex + 1 267 currentHost := host 268 var previousContainer *api.MachineStatus 269 for nextIndex < len(machines) { 270 machine := machines[nextIndex] 271 container := context.makeMachineStatus(machine) 272 if currentHost.Id == state.ParentId(machine.Id()) { 273 currentHost.Containers[machine.Id()] = container 274 previousContainer = &container 275 nextIndex++ 276 } else { 277 if state.NestingLevel(machine.Id()) > state.NestingLevel(previousContainer.Id) { 278 nextIndex = context.processMachine(machines, previousContainer, nextIndex-1) 279 } else { 280 break 281 } 282 } 283 } 284 return 285 } 286 287 func (context *statusContext) makeMachineStatus(machine *state.Machine) (status api.MachineStatus) { 288 status.Id = machine.Id() 289 status.Life, 290 status.AgentVersion, 291 status.AgentState, 292 status.AgentStateInfo, 293 status.Err = processAgent(machine) 294 status.Series = machine.Series() 295 instid, err := machine.InstanceId() 296 if err == nil { 297 status.InstanceId = instid 298 status.InstanceState, err = machine.InstanceStatus() 299 if err != nil { 300 status.InstanceState = "error" 301 } 302 status.DNSName = instance.SelectPublicAddress(machine.Addresses()) 303 } else { 304 if state.IsNotProvisionedError(err) { 305 status.InstanceId = "pending" 306 } else { 307 status.InstanceId = "error" 308 } 309 // There's no point in reporting a pending agent state 310 // if the machine hasn't been provisioned. This 311 // also makes unprovisioned machines visually distinct 312 // in the output. 313 status.AgentState = "" 314 } 315 hc, err := machine.HardwareCharacteristics() 316 if err != nil { 317 if !errors.IsNotFoundError(err) { 318 status.Hardware = "error" 319 } 320 } else { 321 status.Hardware = hc.String() 322 } 323 status.Containers = make(map[string]api.MachineStatus) 324 return 325 } 326 327 func (context *statusContext) processServices() map[string]api.ServiceStatus { 328 servicesMap := make(map[string]api.ServiceStatus) 329 for _, s := range context.services { 330 servicesMap[s.Name()] = context.processService(s) 331 } 332 return servicesMap 333 } 334 335 func (context *statusContext) processService(service *state.Service) (status api.ServiceStatus) { 336 serviceCharmURL, _ := service.CharmURL() 337 status.Charm = serviceCharmURL.String() 338 status.Exposed = service.IsExposed() 339 status.Life = processLife(service) 340 341 latestCharm, ok := context.latestCharms[*serviceCharmURL.WithRevision(-1)] 342 if ok && latestCharm != serviceCharmURL.String() { 343 status.CanUpgradeTo = latestCharm 344 } 345 var err error 346 status.Relations, status.SubordinateTo, err = context.processRelations(service) 347 if err != nil { 348 status.Err = err 349 return 350 } 351 if service.IsPrincipal() { 352 status.Units = context.processUnits(context.units[service.Name()], serviceCharmURL.String()) 353 } 354 return status 355 } 356 357 func (context *statusContext) processUnits(units map[string]*state.Unit, serviceCharm string) map[string]api.UnitStatus { 358 unitsMap := make(map[string]api.UnitStatus) 359 for _, unit := range units { 360 unitsMap[unit.Name()] = context.processUnit(unit, serviceCharm) 361 } 362 return unitsMap 363 } 364 365 func (context *statusContext) processUnit(unit *state.Unit, serviceCharm string) (status api.UnitStatus) { 366 status.PublicAddress, _ = unit.PublicAddress() 367 for _, port := range unit.OpenedPorts() { 368 status.OpenedPorts = append(status.OpenedPorts, port.String()) 369 } 370 if unit.IsPrincipal() { 371 status.Machine, _ = unit.AssignedMachineId() 372 } 373 curl, _ := unit.CharmURL() 374 if serviceCharm != "" && curl != nil && curl.String() != serviceCharm { 375 status.Charm = curl.String() 376 } 377 status.Life, 378 status.AgentVersion, 379 status.AgentState, 380 status.AgentStateInfo, 381 status.Err = processAgent(unit) 382 if subUnits := unit.SubordinateNames(); len(subUnits) > 0 { 383 status.Subordinates = make(map[string]api.UnitStatus) 384 for _, name := range subUnits { 385 subUnit := context.unitByName(name) 386 // subUnit may be nil if subordinate was filtered out. 387 if subUnit != nil { 388 status.Subordinates[name] = context.processUnit(subUnit, serviceCharm) 389 } 390 } 391 } 392 return 393 } 394 395 func (context *statusContext) unitByName(name string) *state.Unit { 396 serviceName := strings.Split(name, "/")[0] 397 return context.units[serviceName][name] 398 } 399 400 func (*statusContext) processRelations(service *state.Service) (related map[string][]string, subord []string, err error) { 401 // TODO(mue) This way the same relation is read twice (for each service). 402 // Maybe add Relations() to state, read them only once and pass them to each 403 // call of this function. 404 relations, err := service.Relations() 405 if err != nil { 406 return nil, nil, mask(err) 407 } 408 var subordSet set.Strings 409 related = make(map[string][]string) 410 for _, relation := range relations { 411 ep, err := relation.Endpoint(service.Name()) 412 if err != nil { 413 return nil, nil, mask(err) 414 } 415 relationName := ep.Relation.Name 416 eps, err := relation.RelatedEndpoints(service.Name()) 417 if err != nil { 418 return nil, nil, mask(err) 419 } 420 for _, ep := range eps { 421 if ep.Scope == charm.ScopeContainer && !service.IsPrincipal() { 422 subordSet.Add(ep.ServiceName) 423 } 424 related[relationName] = append(related[relationName], ep.ServiceName) 425 } 426 } 427 for relationName, serviceNames := range related { 428 sn := set.NewStrings(serviceNames...) 429 related[relationName] = sn.SortedValues() 430 } 431 return related, subordSet.SortedValues(), nil 432 } 433 434 type lifer interface { 435 Life() state.Life 436 } 437 438 type stateAgent interface { 439 lifer 440 AgentAlive() (bool, error) 441 AgentTools() (*tools.Tools, error) 442 Status() (params.Status, string, params.StatusData, error) 443 } 444 445 // processAgent retrieves version and status information from the given entity 446 // and sets the destination version, status and info values accordingly. 447 func processAgent(entity stateAgent) (life string, version string, status params.Status, info string, err error) { 448 life = processLife(entity) 449 if t, err := entity.AgentTools(); err == nil { 450 version = t.Version.Number.String() 451 } 452 // TODO(mue) StatusData may be useful here too. 453 status, info, _, err = entity.Status() 454 if err != nil { 455 return 456 } 457 if status == params.StatusPending { 458 // The status is pending - there's no point 459 // in enquiring about the agent liveness. 460 return 461 } 462 agentAlive, err := entity.AgentAlive() 463 if err != nil { 464 return 465 } 466 if entity.Life() != state.Dead && !agentAlive { 467 // The agent *should* be alive but is not. 468 // Add the original status to the info, so it's not lost. 469 if info != "" { 470 info = fmt.Sprintf("(%s: %s)", status, info) 471 } else { 472 info = fmt.Sprintf("(%s)", status) 473 } 474 status = params.StatusDown 475 } 476 return 477 } 478 479 func processLife(entity lifer) string { 480 if life := entity.Life(); life != state.Alive { 481 // alive is the usual state so omit it by default. 482 return life.String() 483 } 484 return "" 485 }