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  }