github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/status/formatter.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  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/juju/os"
    11  	"github.com/juju/os/series"
    12  	"gopkg.in/juju/charm.v6"
    13  	"gopkg.in/juju/names.v2"
    14  
    15  	"github.com/juju/juju/apiserver/params"
    16  	"github.com/juju/juju/cmd/juju/common"
    17  	"github.com/juju/juju/cmd/juju/storage"
    18  	"github.com/juju/juju/core/status"
    19  	"github.com/juju/juju/state/multiwatcher"
    20  )
    21  
    22  type statusFormatter struct {
    23  	status                 *params.FullStatus
    24  	controllerName         string
    25  	relations              map[int]params.RelationStatus
    26  	storage                *storage.CombinedStorage
    27  	isoTime, showRelations bool
    28  }
    29  
    30  // NewStatusFormatter takes stored model information (params.FullStatus) and populates
    31  // the statusFormatter struct used in various status formatting methods
    32  func NewStatusFormatter(status *params.FullStatus, isoTime bool) *statusFormatter {
    33  	return newStatusFormatter(
    34  		newStatusFormatterParams{
    35  			status:        status,
    36  			isoTime:       isoTime,
    37  			showRelations: true,
    38  		})
    39  }
    40  
    41  type newStatusFormatterParams struct {
    42  	storage                *storage.CombinedStorage
    43  	status                 *params.FullStatus
    44  	controllerName         string
    45  	isoTime, showRelations bool
    46  }
    47  
    48  func newStatusFormatter(p newStatusFormatterParams) *statusFormatter {
    49  	sf := statusFormatter{
    50  		storage:        p.storage,
    51  		status:         p.status,
    52  		controllerName: p.controllerName,
    53  		relations:      make(map[int]params.RelationStatus),
    54  		isoTime:        p.isoTime,
    55  		showRelations:  p.showRelations,
    56  	}
    57  	if p.showRelations {
    58  		for _, relation := range p.status.Relations {
    59  			sf.relations[relation.Id] = relation
    60  		}
    61  	}
    62  	return &sf
    63  }
    64  
    65  func (sf *statusFormatter) format() (formattedStatus, error) {
    66  	if sf.status == nil {
    67  		return formattedStatus{}, nil
    68  	}
    69  	cloudTag, err := names.ParseCloudTag(sf.status.Model.CloudTag)
    70  	if err != nil {
    71  		return formattedStatus{}, err
    72  	}
    73  	out := formattedStatus{
    74  		Model: modelStatus{
    75  			Name:             sf.status.Model.Name,
    76  			Type:             sf.status.Model.Type,
    77  			Controller:       sf.controllerName,
    78  			Cloud:            cloudTag.Id(),
    79  			CloudRegion:      sf.status.Model.CloudRegion,
    80  			Version:          sf.status.Model.Version,
    81  			AvailableVersion: sf.status.Model.AvailableVersion,
    82  			Status:           sf.getStatusInfoContents(sf.status.Model.ModelStatus),
    83  			SLA:              sf.status.Model.SLA,
    84  		},
    85  		Machines:           make(map[string]machineStatus),
    86  		Applications:       make(map[string]applicationStatus),
    87  		RemoteApplications: make(map[string]remoteApplicationStatus),
    88  		Offers:             make(map[string]offerStatus),
    89  		Relations:          make([]relationStatus, len(sf.relations)),
    90  	}
    91  	if sf.status.Model.MeterStatus.Color != "" {
    92  		out.Model.MeterStatus = &meterStatus{
    93  			Color:   sf.status.Model.MeterStatus.Color,
    94  			Message: sf.status.Model.MeterStatus.Message,
    95  		}
    96  	}
    97  	if sf.status.ControllerTimestamp != nil {
    98  		out.Controller = &controllerStatus{
    99  			Timestamp: common.FormatTimeAsTimestamp(sf.status.ControllerTimestamp, sf.isoTime),
   100  		}
   101  	}
   102  	for k, m := range sf.status.Machines {
   103  		out.Machines[k] = sf.formatMachine(m)
   104  	}
   105  	for sn, s := range sf.status.Applications {
   106  		out.Applications[sn] = sf.formatApplication(sn, s)
   107  	}
   108  	for sn, s := range sf.status.RemoteApplications {
   109  		out.RemoteApplications[sn] = sf.formatRemoteApplication(sn, s)
   110  	}
   111  	for name, offer := range sf.status.Offers {
   112  		out.Offers[name] = sf.formatOffer(name, offer)
   113  	}
   114  	i := 0
   115  	for _, rel := range sf.relations {
   116  		out.Relations[i] = sf.formatRelation(rel)
   117  		i++
   118  	}
   119  	if sf.storage != nil {
   120  		out.Storage = sf.storage
   121  	}
   122  	return out, nil
   123  }
   124  
   125  // MachineFormat takes stored model information (params.FullStatus) and formats machine status info.
   126  func (sf *statusFormatter) MachineFormat(machineId []string) formattedMachineStatus {
   127  	if sf.status == nil {
   128  		return formattedMachineStatus{}
   129  	}
   130  	out := formattedMachineStatus{
   131  		Model:    sf.status.Model.Name,
   132  		Machines: make(map[string]machineStatus),
   133  	}
   134  	for k, m := range sf.status.Machines {
   135  		if len(machineId) != 0 {
   136  			for i := 0; i < len(machineId); i++ {
   137  				if m.Id == machineId[i] {
   138  					out.Machines[k] = sf.formatMachine(m)
   139  				}
   140  			}
   141  		} else {
   142  			out.Machines[k] = sf.formatMachine(m)
   143  		}
   144  	}
   145  	return out
   146  }
   147  
   148  func (sf *statusFormatter) formatMachine(machine params.MachineStatus) machineStatus {
   149  	var out machineStatus
   150  
   151  	out = machineStatus{
   152  		JujuStatus:        sf.getStatusInfoContents(machine.AgentStatus),
   153  		DNSName:           machine.DNSName,
   154  		IPAddresses:       machine.IPAddresses,
   155  		InstanceId:        machine.InstanceId,
   156  		DisplayName:       machine.DisplayName,
   157  		MachineStatus:     sf.getStatusInfoContents(machine.InstanceStatus),
   158  		Series:            machine.Series,
   159  		Id:                machine.Id,
   160  		NetworkInterfaces: make(map[string]networkInterface),
   161  		Containers:        make(map[string]machineStatus),
   162  		Constraints:       machine.Constraints,
   163  		Hardware:          machine.Hardware,
   164  		LXDProfiles:       make(map[string]lxdProfileContents),
   165  	}
   166  
   167  	for k, d := range machine.NetworkInterfaces {
   168  		out.NetworkInterfaces[k] = networkInterface{
   169  			IPAddresses:    d.IPAddresses,
   170  			MACAddress:     d.MACAddress,
   171  			Gateway:        d.Gateway,
   172  			DNSNameservers: d.DNSNameservers,
   173  			Space:          d.Space,
   174  			IsUp:           d.IsUp,
   175  		}
   176  	}
   177  
   178  	for k, m := range machine.Containers {
   179  		out.Containers[k] = sf.formatMachine(m)
   180  	}
   181  
   182  	for _, job := range machine.Jobs {
   183  		if job == multiwatcher.JobManageModel {
   184  			out.HAStatus = makeHAStatus(machine.HasVote, machine.WantsVote)
   185  			break
   186  		}
   187  	}
   188  
   189  	for k, v := range machine.LXDProfiles {
   190  		out.LXDProfiles[k] = lxdProfileContents{
   191  			Config:      v.Config,
   192  			Description: v.Description,
   193  			Devices:     v.Devices,
   194  		}
   195  	}
   196  
   197  	return out
   198  }
   199  
   200  func (sf *statusFormatter) formatApplication(name string, application params.ApplicationStatus) applicationStatus {
   201  	var osInfo string
   202  	appOS, _ := series.GetOSFromSeries(application.Series)
   203  	osInfo = strings.ToLower(appOS.String())
   204  
   205  	// TODO(caas) - enhance GetOSFromSeries
   206  	if appOS == os.Unknown && sf.status.Model.Type == "caas" {
   207  		osInfo = application.Series
   208  	}
   209  	var (
   210  		charmOrigin = ""
   211  		charmName   = ""
   212  		charmRev    = 0
   213  	)
   214  	if curl, err := charm.ParseURL(application.Charm); err != nil {
   215  		// We should never fail to parse a charm url sent back
   216  		// but if we do, don't crash.
   217  		logger.Errorf("failed to parse charm: %v", err)
   218  	} else {
   219  		switch curl.Schema {
   220  		case "cs":
   221  			charmOrigin = "jujucharms"
   222  		case "local":
   223  			charmOrigin = "local"
   224  		default:
   225  			charmOrigin = "unknown"
   226  		}
   227  		charmName = curl.Name
   228  		charmRev = curl.Revision
   229  	}
   230  
   231  	out := applicationStatus{
   232  		Err:              application.Err,
   233  		Charm:            application.Charm,
   234  		Series:           application.Series,
   235  		OS:               osInfo,
   236  		CharmOrigin:      charmOrigin,
   237  		CharmName:        charmName,
   238  		CharmRev:         charmRev,
   239  		CharmVersion:     application.CharmVersion,
   240  		Exposed:          application.Exposed,
   241  		Life:             application.Life,
   242  		Scale:            application.Scale,
   243  		Placement:        application.Placement,
   244  		ProviderId:       application.ProviderId,
   245  		Address:          application.PublicAddress,
   246  		Relations:        application.Relations,
   247  		CanUpgradeTo:     application.CanUpgradeTo,
   248  		SubordinateTo:    application.SubordinateTo,
   249  		Units:            make(map[string]unitStatus),
   250  		StatusInfo:       sf.getApplicationStatusInfo(application),
   251  		Version:          application.WorkloadVersion,
   252  		EndpointBindings: application.EndpointBindings,
   253  	}
   254  	for k, m := range application.Units {
   255  		out.Units[k] = sf.formatUnit(unitFormatInfo{
   256  			unit:            m,
   257  			unitName:        k,
   258  			applicationName: name,
   259  			meterStatuses:   application.MeterStatuses,
   260  		})
   261  	}
   262  
   263  	return out
   264  }
   265  
   266  func (sf *statusFormatter) formatRemoteApplication(name string, application params.RemoteApplicationStatus) remoteApplicationStatus {
   267  	out := remoteApplicationStatus{
   268  		Err:        application.Err,
   269  		OfferURL:   application.OfferURL,
   270  		Life:       application.Life,
   271  		Relations:  application.Relations,
   272  		StatusInfo: sf.getRemoteApplicationStatusInfo(application),
   273  	}
   274  	out.Endpoints = make(map[string]remoteEndpoint)
   275  	for _, ep := range application.Endpoints {
   276  		out.Endpoints[ep.Name] = remoteEndpoint{
   277  			Interface: ep.Interface,
   278  			Role:      string(ep.Role),
   279  		}
   280  	}
   281  	return out
   282  }
   283  
   284  func (sf *statusFormatter) formatRelation(rel params.RelationStatus) relationStatus {
   285  	var provider, requirer params.EndpointStatus
   286  	for _, ep := range rel.Endpoints {
   287  		switch charm.RelationRole(ep.Role) {
   288  		case charm.RolePeer:
   289  			provider = ep
   290  			requirer = ep
   291  		case charm.RoleProvider:
   292  			provider = ep
   293  		case charm.RoleRequirer:
   294  			requirer = ep
   295  		}
   296  	}
   297  	var relType string
   298  	switch {
   299  	case rel.Scope == "container":
   300  		relType = "subordinate"
   301  	case provider.ApplicationName == requirer.ApplicationName:
   302  		relType = "peer"
   303  	default:
   304  		relType = "regular"
   305  	}
   306  	out := relationStatus{
   307  		Provider:  fmt.Sprintf("%s:%s", provider.ApplicationName, provider.Name),
   308  		Requirer:  fmt.Sprintf("%s:%s", requirer.ApplicationName, requirer.Name),
   309  		Interface: rel.Interface,
   310  		Type:      relType,
   311  		Status:    rel.Status.Status,
   312  		Message:   rel.Status.Info,
   313  	}
   314  	return out
   315  }
   316  
   317  func (sf *statusFormatter) getApplicationStatusInfo(application params.ApplicationStatus) statusInfoContents {
   318  	// TODO(perrito66) add status validation.
   319  	info := statusInfoContents{
   320  		Err:     application.Status.Err,
   321  		Current: status.Status(application.Status.Status),
   322  		Message: application.Status.Info,
   323  		Version: application.Status.Version,
   324  	}
   325  	if application.Status.Since != nil {
   326  		info.Since = common.FormatTime(application.Status.Since, sf.isoTime)
   327  	}
   328  	return info
   329  }
   330  
   331  func (sf *statusFormatter) getRemoteApplicationStatusInfo(application params.RemoteApplicationStatus) statusInfoContents {
   332  	// TODO(perrito66) add status validation.
   333  	info := statusInfoContents{
   334  		Err:     application.Status.Err,
   335  		Current: status.Status(application.Status.Status),
   336  		Message: application.Status.Info,
   337  		Version: application.Status.Version,
   338  	}
   339  	if application.Status.Since != nil {
   340  		info.Since = common.FormatTime(application.Status.Since, sf.isoTime)
   341  	}
   342  	return info
   343  }
   344  
   345  func (sf *statusFormatter) formatOffer(name string, offer params.ApplicationOfferStatus) offerStatus {
   346  	out := offerStatus{
   347  		Err:                  offer.Err,
   348  		ApplicationName:      offer.ApplicationName,
   349  		CharmURL:             offer.CharmURL,
   350  		ActiveConnectedCount: offer.ActiveConnectedCount,
   351  		TotalConnectedCount:  offer.TotalConnectedCount,
   352  	}
   353  	out.Endpoints = make(map[string]remoteEndpoint)
   354  	for alias, ep := range offer.Endpoints {
   355  		out.Endpoints[alias] = remoteEndpoint{
   356  			Name:      ep.Name,
   357  			Interface: ep.Interface,
   358  			Role:      string(ep.Role),
   359  		}
   360  	}
   361  	return out
   362  }
   363  
   364  type unitFormatInfo struct {
   365  	unit            params.UnitStatus
   366  	unitName        string
   367  	applicationName string
   368  	meterStatuses   map[string]params.MeterStatus
   369  }
   370  
   371  func (sf *statusFormatter) formatUnit(info unitFormatInfo) unitStatus {
   372  	// TODO(Wallyworld) - this should be server side but we still need to support older servers.
   373  	sf.updateUnitStatusInfo(&info.unit, info.applicationName)
   374  
   375  	out := unitStatus{
   376  		WorkloadStatusInfo: sf.getWorkloadStatusInfo(info.unit),
   377  		JujuStatusInfo:     sf.getAgentStatusInfo(info.unit),
   378  		Machine:            info.unit.Machine,
   379  		OpenedPorts:        info.unit.OpenedPorts,
   380  		ProviderId:         info.unit.ProviderId,
   381  		Address:            info.unit.Address,
   382  		PublicAddress:      info.unit.PublicAddress,
   383  		Charm:              info.unit.Charm,
   384  		Subordinates:       make(map[string]unitStatus),
   385  		Leader:             info.unit.Leader,
   386  	}
   387  
   388  	if ms, ok := info.meterStatuses[info.unitName]; ok {
   389  		out.MeterStatus = &meterStatus{
   390  			Color:   ms.Color,
   391  			Message: ms.Message,
   392  		}
   393  	}
   394  
   395  	for k, m := range info.unit.Subordinates {
   396  		out.Subordinates[k] = sf.formatUnit(unitFormatInfo{
   397  			unit:            m,
   398  			unitName:        k,
   399  			applicationName: info.applicationName,
   400  			meterStatuses:   info.meterStatuses,
   401  		})
   402  	}
   403  	return out
   404  }
   405  
   406  func (sf *statusFormatter) getStatusInfoContents(inst params.DetailedStatus) statusInfoContents {
   407  	// TODO(perrito66) add status validation.
   408  	info := statusInfoContents{
   409  		Err:     inst.Err,
   410  		Current: status.Status(inst.Status),
   411  		Message: inst.Info,
   412  		Version: inst.Version,
   413  		Life:    inst.Life,
   414  	}
   415  	if inst.Since != nil {
   416  		info.Since = common.FormatTime(inst.Since, sf.isoTime)
   417  	}
   418  	return info
   419  }
   420  
   421  func (sf *statusFormatter) getWorkloadStatusInfo(unit params.UnitStatus) statusInfoContents {
   422  	if unit.WorkloadStatus.Status == "" {
   423  		return statusInfoContents{}
   424  	}
   425  	// TODO(perrito66) add status validation.
   426  	info := statusInfoContents{
   427  		Err:     unit.WorkloadStatus.Err,
   428  		Current: status.Status(unit.WorkloadStatus.Status),
   429  		Message: unit.WorkloadStatus.Info,
   430  		Version: unit.WorkloadStatus.Version,
   431  	}
   432  	if unit.WorkloadStatus.Since != nil {
   433  		info.Since = common.FormatTime(unit.WorkloadStatus.Since, sf.isoTime)
   434  	}
   435  	return info
   436  }
   437  
   438  func (sf *statusFormatter) getAgentStatusInfo(unit params.UnitStatus) statusInfoContents {
   439  	// TODO(perrito66) add status validation.
   440  	info := statusInfoContents{
   441  		Err:     unit.AgentStatus.Err,
   442  		Current: status.Status(unit.AgentStatus.Status),
   443  		Message: unit.AgentStatus.Info,
   444  		Version: unit.AgentStatus.Version,
   445  	}
   446  	if unit.AgentStatus.Since != nil {
   447  		info.Since = common.FormatTime(unit.AgentStatus.Since, sf.isoTime)
   448  	}
   449  	return info
   450  }
   451  
   452  func (sf *statusFormatter) updateUnitStatusInfo(unit *params.UnitStatus, applicationName string) {
   453  	// TODO(perrito66) add status validation.
   454  	if status.Status(unit.WorkloadStatus.Status) == status.Error {
   455  		if relation, ok := sf.relations[getRelationIdFromData(unit)]; ok {
   456  			// Append the details of the other endpoint on to the status info string.
   457  			if ep, ok := findOtherEndpoint(relation.Endpoints, applicationName); ok {
   458  				unit.WorkloadStatus.Info = unit.WorkloadStatus.Info + " for " + ep.String()
   459  			}
   460  		}
   461  	}
   462  }
   463  
   464  func makeHAStatus(hasVote, wantsVote bool) string {
   465  	var s string
   466  	switch {
   467  	case hasVote && wantsVote:
   468  		s = "has-vote"
   469  	case hasVote && !wantsVote:
   470  		s = "removing-vote"
   471  	case !hasVote && wantsVote:
   472  		s = "adding-vote"
   473  	case !hasVote && !wantsVote:
   474  		s = "no-vote"
   475  	}
   476  	return s
   477  }
   478  
   479  func getRelationIdFromData(unit *params.UnitStatus) int {
   480  	if relationId_, ok := unit.WorkloadStatus.Data["relation-id"]; ok {
   481  		if relationId, ok := relationId_.(float64); ok {
   482  			return int(relationId)
   483  		} else {
   484  			logger.Infof("relation-id found status data but was unexpected "+
   485  				"type: %q. Status output may be lacking some detail.", relationId_)
   486  		}
   487  	}
   488  	return -1
   489  }
   490  
   491  // findOtherEndpoint searches the provided endpoints for an endpoint
   492  // that *doesn't* match applicationName. The returned bool indicates if
   493  // such an endpoint was found.
   494  func findOtherEndpoint(endpoints []params.EndpointStatus, applicationName string) (params.EndpointStatus, bool) {
   495  	for _, endpoint := range endpoints {
   496  		if endpoint.ApplicationName != applicationName {
   497  			return endpoint, true
   498  		}
   499  	}
   500  	return params.EndpointStatus{}, false
   501  }