github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/state/apiserver/client/status.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package client 5 6 import ( 7 "fmt" 8 "path" 9 "regexp" 10 "strings" 11 12 "github.com/juju/errors" 13 "github.com/juju/utils/set" 14 15 "github.com/juju/juju/charm" 16 "github.com/juju/juju/constraints" 17 "github.com/juju/juju/instance" 18 "github.com/juju/juju/juju" 19 "github.com/juju/juju/state" 20 "github.com/juju/juju/state/api" 21 "github.com/juju/juju/state/api/params" 22 "github.com/juju/juju/tools" 23 ) 24 25 // FullStatus gives the information needed for juju status over the api 26 func (c *Client) FullStatus(args params.StatusParams) (api.Status, error) { 27 conn, err := juju.NewConnFromState(c.api.state) 28 if err != nil { 29 return api.Status{}, err 30 } 31 var noStatus api.Status 32 var context statusContext 33 unitMatcher, err := NewUnitMatcher(args.Patterns) 34 if err != nil { 35 return noStatus, err 36 } 37 if context.services, 38 context.units, context.latestCharms, err = fetchAllServicesAndUnits(conn.State, unitMatcher); err != nil { 39 return noStatus, err 40 } 41 42 // Filter machines by units in scope. 43 var machineIds *set.Strings 44 if !unitMatcher.matchesAny() { 45 machineIds, err = fetchUnitMachineIds(context.units) 46 if err != nil { 47 return noStatus, err 48 } 49 } 50 if context.machines, err = fetchMachines(conn.State, machineIds); err != nil { 51 return noStatus, err 52 } 53 if context.relations, err = fetchRelations(conn.State); err != nil { 54 return noStatus, err 55 } 56 if context.networks, err = fetchNetworks(conn.State); err != nil { 57 return noStatus, err 58 } 59 60 return api.Status{ 61 EnvironmentName: conn.Environ.Name(), 62 Machines: context.processMachines(), 63 Services: context.processServices(), 64 Networks: context.processNetworks(), 65 Relations: context.processRelations(), 66 }, nil 67 } 68 69 // Status is a stub version of FullStatus that was introduced in 1.16 70 func (c *Client) Status() (api.LegacyStatus, error) { 71 var legacyStatus api.LegacyStatus 72 status, err := c.FullStatus(params.StatusParams{}) 73 if err != nil { 74 return legacyStatus, err 75 } 76 77 legacyStatus.Machines = make(map[string]api.LegacyMachineStatus) 78 for machineName, machineStatus := range status.Machines { 79 legacyStatus.Machines[machineName] = api.LegacyMachineStatus{ 80 InstanceId: string(machineStatus.InstanceId), 81 } 82 } 83 return legacyStatus, nil 84 } 85 86 type statusContext struct { 87 machines map[string][]*state.Machine 88 services map[string]*state.Service 89 relations map[string][]*state.Relation 90 units map[string]map[string]*state.Unit 91 networks map[string]*state.Network 92 latestCharms map[charm.URL]string 93 } 94 95 type unitMatcher struct { 96 patterns []string 97 } 98 99 // matchesAny returns true if the unitMatcher will 100 // match any unit, regardless of its attributes. 101 func (m unitMatcher) matchesAny() bool { 102 return len(m.patterns) == 0 103 } 104 105 // matchUnit attempts to match a state.Unit to one of 106 // a set of patterns, taking into account subordinate 107 // relationships. 108 func (m unitMatcher) matchUnit(u *state.Unit) bool { 109 if m.matchesAny() { 110 return true 111 } 112 113 // Keep the unit if: 114 // (a) its name matches a pattern, or 115 // (b) it's a principal and one of its subordinates matches, or 116 // (c) it's a subordinate and its principal matches. 117 // 118 // Note: do *not* include a second subordinate if the principal is 119 // only matched on account of a first subordinate matching. 120 if m.matchString(u.Name()) { 121 return true 122 } 123 if u.IsPrincipal() { 124 for _, s := range u.SubordinateNames() { 125 if m.matchString(s) { 126 return true 127 } 128 } 129 return false 130 } 131 principal, valid := u.PrincipalName() 132 if !valid { 133 panic("PrincipalName failed for subordinate unit") 134 } 135 return m.matchString(principal) 136 } 137 138 // matchString matches a string to one of the patterns in 139 // the unit matcher, returning an error if a pattern with 140 // invalid syntax is encountered. 141 func (m unitMatcher) matchString(s string) bool { 142 for _, pattern := range m.patterns { 143 ok, err := path.Match(pattern, s) 144 if err != nil { 145 // We validate patterns, so should never get here. 146 panic(fmt.Errorf("pattern syntax error in %q", pattern)) 147 } else if ok { 148 return true 149 } 150 } 151 return false 152 } 153 154 // validPattern must match the parts of a unit or service name 155 // pattern either side of the '/' for it to be valid. 156 var validPattern = regexp.MustCompile("^[a-z0-9-*]+$") 157 158 // NewUnitMatcher returns a unitMatcher that matches units 159 // with one of the specified patterns, or all units if no 160 // patterns are specified. 161 // 162 // An error will be returned if any of the specified patterns 163 // is invalid. Patterns are valid if they contain only 164 // alpha-numeric characters, hyphens, or asterisks (and one 165 // optional '/' to separate service/unit). 166 func NewUnitMatcher(patterns []string) (unitMatcher, error) { 167 for i, pattern := range patterns { 168 fields := strings.Split(pattern, "/") 169 if len(fields) > 2 { 170 return unitMatcher{}, fmt.Errorf("pattern %q contains too many '/' characters", pattern) 171 } 172 for _, f := range fields { 173 if !validPattern.MatchString(f) { 174 return unitMatcher{}, fmt.Errorf("pattern %q contains invalid characters", pattern) 175 } 176 } 177 if len(fields) == 1 { 178 patterns[i] += "/*" 179 } 180 } 181 return unitMatcher{patterns}, nil 182 } 183 184 // fetchMachines returns a map from top level machine id to machines, where machines[0] is the host 185 // machine and machines[1..n] are any containers (including nested ones). 186 // 187 // If machineIds is non-nil, only machines whose IDs are in the set are returned. 188 func fetchMachines(st *state.State, machineIds *set.Strings) (map[string][]*state.Machine, error) { 189 v := make(map[string][]*state.Machine) 190 machines, err := st.AllMachines() 191 if err != nil { 192 return nil, err 193 } 194 // AllMachines gives us machines sorted by id. 195 for _, m := range machines { 196 if machineIds != nil && !machineIds.Contains(m.Id()) { 197 continue 198 } 199 parentId, ok := m.ParentId() 200 if !ok { 201 // Only top level host machines go directly into the machine map. 202 v[m.Id()] = []*state.Machine{m} 203 } else { 204 topParentId := state.TopParentId(m.Id()) 205 machines, ok := v[topParentId] 206 if !ok { 207 panic(fmt.Errorf("unexpected machine id %q", parentId)) 208 } 209 machines = append(machines, m) 210 v[topParentId] = machines 211 } 212 } 213 return v, nil 214 } 215 216 // fetchAllServicesAndUnits returns a map from service name to service, 217 // a map from service name to unit name to unit, and a map from base charm URL to latest URL. 218 func fetchAllServicesAndUnits( 219 st *state.State, unitMatcher unitMatcher) ( 220 map[string]*state.Service, map[string]map[string]*state.Unit, map[charm.URL]string, error) { 221 222 svcMap := make(map[string]*state.Service) 223 unitMap := make(map[string]map[string]*state.Unit) 224 latestCharms := make(map[charm.URL]string) 225 services, err := st.AllServices() 226 if err != nil { 227 return nil, nil, nil, err 228 } 229 for _, s := range services { 230 units, err := s.AllUnits() 231 if err != nil { 232 return nil, nil, nil, err 233 } 234 svcUnitMap := make(map[string]*state.Unit) 235 for _, u := range units { 236 if !unitMatcher.matchUnit(u) { 237 continue 238 } 239 svcUnitMap[u.Name()] = u 240 } 241 if unitMatcher.matchesAny() || len(svcUnitMap) > 0 { 242 unitMap[s.Name()] = svcUnitMap 243 svcMap[s.Name()] = s 244 // Record the base URL for the service's charm so that 245 // the latest store revision can be looked up. 246 charmURL, _ := s.CharmURL() 247 if charmURL.Schema == "cs" { 248 latestCharms[*charmURL.WithRevision(-1)] = "" 249 } 250 } 251 } 252 for baseURL, _ := range latestCharms { 253 ch, err := st.LatestPlaceholderCharm(&baseURL) 254 if errors.IsNotFound(err) { 255 continue 256 } 257 if err != nil { 258 return nil, nil, nil, err 259 } 260 latestCharms[baseURL] = ch.String() 261 } 262 return svcMap, unitMap, latestCharms, nil 263 } 264 265 // fetchUnitMachineIds returns a set of IDs for machines that 266 // the specified units reside on, and those machines' ancestors. 267 func fetchUnitMachineIds(units map[string]map[string]*state.Unit) (*set.Strings, error) { 268 machineIds := new(set.Strings) 269 for _, svcUnitMap := range units { 270 for _, unit := range svcUnitMap { 271 if !unit.IsPrincipal() { 272 continue 273 } 274 mid, err := unit.AssignedMachineId() 275 if err != nil { 276 return nil, err 277 } 278 for mid != "" { 279 machineIds.Add(mid) 280 mid = state.ParentId(mid) 281 } 282 } 283 } 284 return machineIds, nil 285 } 286 287 // fetchRelations returns a map of all relations keyed by service name. 288 // 289 // This structure is useful for processServiceRelations() which needs 290 // to have the relations for each service. Reading them once here 291 // avoids the repeated DB hits to retrieve the relations for each 292 // service that used to happen in processServiceRelations(). 293 func fetchRelations(st *state.State) (map[string][]*state.Relation, error) { 294 relations, err := st.AllRelations() 295 if err != nil { 296 return nil, err 297 } 298 out := make(map[string][]*state.Relation) 299 for _, relation := range relations { 300 for _, ep := range relation.Endpoints() { 301 out[ep.ServiceName] = append(out[ep.ServiceName], relation) 302 } 303 } 304 return out, nil 305 } 306 307 // fetchNetworks returns a map from network name to network. 308 func fetchNetworks(st *state.State) (map[string]*state.Network, error) { 309 networks, err := st.AllNetworks() 310 if err != nil { 311 return nil, err 312 } 313 out := make(map[string]*state.Network) 314 for _, n := range networks { 315 out[n.Name()] = n 316 } 317 return out, nil 318 } 319 320 func (context *statusContext) processMachines() map[string]api.MachineStatus { 321 machinesMap := make(map[string]api.MachineStatus) 322 for id, machines := range context.machines { 323 hostStatus := context.makeMachineStatus(machines[0]) 324 context.processMachine(machines, &hostStatus, 0) 325 machinesMap[id] = hostStatus 326 } 327 return machinesMap 328 } 329 330 func (context *statusContext) processMachine(machines []*state.Machine, host *api.MachineStatus, startIndex int) (nextIndex int) { 331 nextIndex = startIndex + 1 332 currentHost := host 333 var previousContainer *api.MachineStatus 334 for nextIndex < len(machines) { 335 machine := machines[nextIndex] 336 container := context.makeMachineStatus(machine) 337 if currentHost.Id == state.ParentId(machine.Id()) { 338 currentHost.Containers[machine.Id()] = container 339 previousContainer = &container 340 nextIndex++ 341 } else { 342 if state.NestingLevel(machine.Id()) > state.NestingLevel(previousContainer.Id) { 343 nextIndex = context.processMachine(machines, previousContainer, nextIndex-1) 344 } else { 345 break 346 } 347 } 348 } 349 return 350 } 351 352 func (context *statusContext) makeMachineStatus(machine *state.Machine) (status api.MachineStatus) { 353 status.Id = machine.Id() 354 status.Agent, status.AgentState, status.AgentStateInfo = processAgent(machine) 355 status.AgentVersion = status.Agent.Version 356 status.Life = status.Agent.Life 357 status.Err = status.Agent.Err 358 status.Series = machine.Series() 359 status.Jobs = paramsJobsFromJobs(machine.Jobs()) 360 status.WantsVote = machine.WantsVote() 361 status.HasVote = machine.HasVote() 362 instid, err := machine.InstanceId() 363 if err == nil { 364 status.InstanceId = instid 365 status.InstanceState, err = machine.InstanceStatus() 366 if err != nil { 367 status.InstanceState = "error" 368 } 369 status.DNSName = instance.SelectPublicAddress(machine.Addresses()) 370 } else { 371 if state.IsNotProvisionedError(err) { 372 status.InstanceId = "pending" 373 } else { 374 status.InstanceId = "error" 375 } 376 // There's no point in reporting a pending agent state 377 // if the machine hasn't been provisioned. This 378 // also makes unprovisioned machines visually distinct 379 // in the output. 380 status.AgentState = "" 381 } 382 hc, err := machine.HardwareCharacteristics() 383 if err != nil { 384 if !errors.IsNotFound(err) { 385 status.Hardware = "error" 386 } 387 } else { 388 status.Hardware = hc.String() 389 } 390 status.Containers = make(map[string]api.MachineStatus) 391 return 392 } 393 394 func (context *statusContext) processRelations() []api.RelationStatus { 395 var out []api.RelationStatus 396 relations := context.getAllRelations() 397 for _, relation := range relations { 398 var eps []api.EndpointStatus 399 var scope charm.RelationScope 400 var relationInterface string 401 for _, ep := range relation.Endpoints() { 402 eps = append(eps, api.EndpointStatus{ 403 ServiceName: ep.ServiceName, 404 Name: ep.Name, 405 Role: ep.Role, 406 Subordinate: context.isSubordinate(&ep), 407 }) 408 // these should match on both sides so use the last 409 relationInterface = ep.Interface 410 scope = ep.Scope 411 } 412 relStatus := api.RelationStatus{ 413 Id: relation.Id(), 414 Key: relation.String(), 415 Interface: relationInterface, 416 Scope: scope, 417 Endpoints: eps, 418 } 419 out = append(out, relStatus) 420 } 421 return out 422 } 423 424 // This method exists only to dedup the loaded relations as they will 425 // appear multiple times in context.relations. 426 func (context *statusContext) getAllRelations() []*state.Relation { 427 var out []*state.Relation 428 seenRelations := make(map[int]bool) 429 for _, relations := range context.relations { 430 for _, relation := range relations { 431 if _, found := seenRelations[relation.Id()]; !found { 432 out = append(out, relation) 433 seenRelations[relation.Id()] = true 434 } 435 } 436 } 437 return out 438 } 439 440 func (context *statusContext) processNetworks() map[string]api.NetworkStatus { 441 networksMap := make(map[string]api.NetworkStatus) 442 for name, network := range context.networks { 443 networksMap[name] = context.makeNetworkStatus(network) 444 } 445 return networksMap 446 } 447 448 func (context *statusContext) makeNetworkStatus(network *state.Network) api.NetworkStatus { 449 return api.NetworkStatus{ 450 ProviderId: network.ProviderId(), 451 CIDR: network.CIDR(), 452 VLANTag: network.VLANTag(), 453 } 454 } 455 456 func (context *statusContext) isSubordinate(ep *state.Endpoint) bool { 457 service := context.services[ep.ServiceName] 458 if service == nil { 459 return false 460 } 461 return isSubordinate(ep, service) 462 } 463 464 func isSubordinate(ep *state.Endpoint, service *state.Service) bool { 465 return ep.Scope == charm.ScopeContainer && !service.IsPrincipal() 466 } 467 468 // paramsJobsFromJobs converts state jobs to params jobs. 469 func paramsJobsFromJobs(jobs []state.MachineJob) []params.MachineJob { 470 paramsJobs := make([]params.MachineJob, len(jobs)) 471 for i, machineJob := range jobs { 472 paramsJobs[i] = machineJob.ToParams() 473 } 474 return paramsJobs 475 } 476 477 func (context *statusContext) processServices() map[string]api.ServiceStatus { 478 servicesMap := make(map[string]api.ServiceStatus) 479 for _, s := range context.services { 480 servicesMap[s.Name()] = context.processService(s) 481 } 482 return servicesMap 483 } 484 485 func (context *statusContext) processService(service *state.Service) (status api.ServiceStatus) { 486 serviceCharmURL, _ := service.CharmURL() 487 status.Charm = serviceCharmURL.String() 488 status.Exposed = service.IsExposed() 489 status.Life = processLife(service) 490 491 latestCharm, ok := context.latestCharms[*serviceCharmURL.WithRevision(-1)] 492 if ok && latestCharm != serviceCharmURL.String() { 493 status.CanUpgradeTo = latestCharm 494 } 495 var err error 496 status.Relations, status.SubordinateTo, err = context.processServiceRelations(service) 497 if err != nil { 498 status.Err = err 499 return 500 } 501 networks, err := service.Networks() 502 if err != nil { 503 status.Err = err 504 return 505 } 506 var cons constraints.Value 507 if service.IsPrincipal() { 508 // Only principals can have constraints. 509 cons, err = service.Constraints() 510 if err != nil { 511 status.Err = err 512 return 513 } 514 } 515 if len(networks) > 0 || cons.HaveNetworks() { 516 // Only the explicitly requested networks (using "juju deploy 517 // <svc> --networks=...") will be enabled, and altough when 518 // specified, networks constraints will be used for instance 519 // selection, they won't be actually enabled. 520 status.Networks = api.NetworksSpecification{ 521 Enabled: networks, 522 Disabled: append(cons.IncludeNetworks(), cons.ExcludeNetworks()...), 523 } 524 } 525 if service.IsPrincipal() { 526 status.Units = context.processUnits(context.units[service.Name()], serviceCharmURL.String()) 527 } 528 return status 529 } 530 531 func (context *statusContext) processUnits(units map[string]*state.Unit, serviceCharm string) map[string]api.UnitStatus { 532 unitsMap := make(map[string]api.UnitStatus) 533 for _, unit := range units { 534 unitsMap[unit.Name()] = context.processUnit(unit, serviceCharm) 535 } 536 return unitsMap 537 } 538 539 func (context *statusContext) processUnit(unit *state.Unit, serviceCharm string) (status api.UnitStatus) { 540 status.PublicAddress, _ = unit.PublicAddress() 541 for _, port := range unit.OpenedPorts() { 542 status.OpenedPorts = append(status.OpenedPorts, port.String()) 543 } 544 if unit.IsPrincipal() { 545 status.Machine, _ = unit.AssignedMachineId() 546 } 547 curl, _ := unit.CharmURL() 548 if serviceCharm != "" && curl != nil && curl.String() != serviceCharm { 549 status.Charm = curl.String() 550 } 551 status.Agent, status.AgentState, status.AgentStateInfo = processAgent(unit) 552 status.AgentVersion = status.Agent.Version 553 status.Life = status.Agent.Life 554 status.Err = status.Agent.Err 555 if subUnits := unit.SubordinateNames(); len(subUnits) > 0 { 556 status.Subordinates = make(map[string]api.UnitStatus) 557 for _, name := range subUnits { 558 subUnit := context.unitByName(name) 559 // subUnit may be nil if subordinate was filtered out. 560 if subUnit != nil { 561 status.Subordinates[name] = context.processUnit(subUnit, serviceCharm) 562 } 563 } 564 } 565 return 566 } 567 568 func (context *statusContext) unitByName(name string) *state.Unit { 569 serviceName := strings.Split(name, "/")[0] 570 return context.units[serviceName][name] 571 } 572 573 func (context *statusContext) processServiceRelations(service *state.Service) ( 574 related map[string][]string, subord []string, err error) { 575 var subordSet set.Strings 576 related = make(map[string][]string) 577 relations := context.relations[service.Name()] 578 for _, relation := range relations { 579 ep, err := relation.Endpoint(service.Name()) 580 if err != nil { 581 return nil, nil, err 582 } 583 relationName := ep.Relation.Name 584 eps, err := relation.RelatedEndpoints(service.Name()) 585 if err != nil { 586 return nil, nil, err 587 } 588 for _, ep := range eps { 589 if isSubordinate(&ep, service) { 590 subordSet.Add(ep.ServiceName) 591 } 592 related[relationName] = append(related[relationName], ep.ServiceName) 593 } 594 } 595 for relationName, serviceNames := range related { 596 sn := set.NewStrings(serviceNames...) 597 related[relationName] = sn.SortedValues() 598 } 599 return related, subordSet.SortedValues(), nil 600 } 601 602 type lifer interface { 603 Life() state.Life 604 } 605 606 type stateAgent interface { 607 lifer 608 AgentAlive() (bool, error) 609 AgentTools() (*tools.Tools, error) 610 Status() (params.Status, string, params.StatusData, error) 611 } 612 613 // processAgent retrieves version and status information from the given entity. 614 func processAgent(entity stateAgent) ( 615 out api.AgentStatus, compatStatus params.Status, compatInfo string) { 616 617 out.Life = processLife(entity) 618 619 if t, err := entity.AgentTools(); err == nil { 620 out.Version = t.Version.Number.String() 621 } 622 623 out.Status, out.Info, out.Data, out.Err = entity.Status() 624 compatStatus = out.Status 625 compatInfo = out.Info 626 out.Data = filterStatusData(out.Data) 627 if out.Err != nil { 628 return 629 } 630 631 if out.Status == params.StatusPending { 632 // The status is pending - there's no point 633 // in enquiring about the agent liveness. 634 return 635 } 636 637 agentAlive, err := entity.AgentAlive() 638 if err != nil { 639 return 640 } 641 642 if entity.Life() != state.Dead && !agentAlive { 643 // The agent *should* be alive but is not. Set status to 644 // StatusDown and munge Info to indicate the previous status and 645 // info. This is unfortunately making presentation decisions 646 // on behalf of the client (crappy). 647 // 648 // This is munging is only being left in place for 649 // compatibility with older clients. TODO: At some point we 650 // should change this so that Info left alone. API version may 651 // help here. 652 // 653 // Better yet, Status shouldn't be changed here in the API at 654 // all! Status changes should only happen in State. One 655 // problem caused by this is that this status change won't be 656 // seen by clients using a watcher because it didn't happen in 657 // State. 658 if out.Info != "" { 659 compatInfo = fmt.Sprintf("(%s: %s)", out.Status, out.Info) 660 } else { 661 compatInfo = fmt.Sprintf("(%s)", out.Status) 662 } 663 compatStatus = params.StatusDown 664 } 665 666 return 667 } 668 669 // filterStatusData limits what agent StatusData data is passed over 670 // the API. This prevents unintended leakage of internal-only data. 671 func filterStatusData(status params.StatusData) params.StatusData { 672 out := make(params.StatusData) 673 for name, value := range status { 674 // use a set here if we end up with a larger whitelist 675 if name == "relation-id" { 676 out[name] = value 677 } 678 } 679 return out 680 } 681 682 func processLife(entity lifer) string { 683 if life := entity.Life(); life != state.Alive { 684 // alive is the usual state so omit it by default. 685 return life.String() 686 } 687 return "" 688 }