github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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  	"fmt"
     8  	"io"
     9  	"regexp"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/juju/ansiterm"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/naturalsort"
    16  	"gopkg.in/juju/charm.v6"
    17  	"gopkg.in/juju/charm.v6/hooks"
    18  
    19  	cmdcrossmodel "github.com/juju/juju/cmd/juju/crossmodel"
    20  	"github.com/juju/juju/cmd/juju/storage"
    21  	"github.com/juju/juju/cmd/output"
    22  	"github.com/juju/juju/core/crossmodel"
    23  	"github.com/juju/juju/core/instance"
    24  	"github.com/juju/juju/core/relation"
    25  	"github.com/juju/juju/core/status"
    26  )
    27  
    28  const (
    29  	caasModelType       = "caas"
    30  	ellipsis            = "..."
    31  	iaasMaxVersionWidth = 15
    32  	caasMaxVersionWidth = 30
    33  )
    34  
    35  // FormatTabular writes a tabular summary of machines, applications, and
    36  // units. Any subordinate items are indented by two spaces beneath
    37  // their superior.
    38  func FormatTabular(writer io.Writer, forceColor bool, value interface{}) error {
    39  	fs, valueConverted := value.(formattedStatus)
    40  	if !valueConverted {
    41  		return errors.Errorf("expected value of type %T, got %T", fs, value)
    42  	}
    43  
    44  	// To format things into columns.
    45  	tw := output.TabWriter(writer)
    46  	if forceColor {
    47  		tw.SetColorCapable(forceColor)
    48  	}
    49  
    50  	cloudRegion := fs.Model.Cloud
    51  	if fs.Model.CloudRegion != "" {
    52  		cloudRegion += "/" + fs.Model.CloudRegion
    53  	}
    54  
    55  	// Default table output
    56  	header := []interface{}{"Model", "Controller", "Cloud/Region", "Version"}
    57  	values := []interface{}{fs.Model.Name, fs.Model.Controller, cloudRegion, fs.Model.Version}
    58  
    59  	// Optional table output if values exist
    60  	message := getModelMessage(fs.Model)
    61  	if fs.Model.SLA != "" {
    62  		header = append(header, "SLA")
    63  		values = append(values, fs.Model.SLA)
    64  	}
    65  	if cs := fs.Controller; cs != nil && cs.Timestamp != "" {
    66  		header = append(header, "Timestamp")
    67  		values = append(values, cs.Timestamp)
    68  	}
    69  	if message != "" {
    70  		header = append(header, "Notes")
    71  		values = append(values, message)
    72  	}
    73  
    74  	// The first set of headers don't use outputHeaders because it adds the blank line.
    75  	w := startSection(tw, true, header...)
    76  	w.Println(values...)
    77  
    78  	if len(fs.RemoteApplications) > 0 {
    79  		printRemoteApplications(tw, fs.RemoteApplications)
    80  	}
    81  
    82  	if len(fs.Applications) > 0 {
    83  		printApplications(tw, fs)
    84  	}
    85  
    86  	if fs.Model.Type != caasModelType && len(fs.Machines) > 0 {
    87  		printMachines(tw, false, fs.Machines)
    88  	}
    89  
    90  	if err := printOffers(tw, fs.Offers); err != nil {
    91  		w.Println(err.Error())
    92  	}
    93  
    94  	if len(fs.Relations) > 0 {
    95  		printRelations(tw, fs.Relations)
    96  	}
    97  
    98  	if fs.Storage != nil {
    99  		storage.FormatStorageListForStatusTabular(tw, *fs.Storage)
   100  	}
   101  
   102  	endSection(tw)
   103  	return nil
   104  }
   105  
   106  func startSection(tw *ansiterm.TabWriter, top bool, headers ...interface{}) output.Wrapper {
   107  	w := output.Wrapper{tw}
   108  	if !top {
   109  		w.Println()
   110  	}
   111  	w.Println(headers...)
   112  	return w
   113  }
   114  
   115  func endSection(tw *ansiterm.TabWriter) {
   116  	tw.Flush()
   117  }
   118  
   119  func printApplications(tw *ansiterm.TabWriter, fs formattedStatus) {
   120  	maxVersionWidth := iaasMaxVersionWidth
   121  	if fs.Model.Type == caasModelType {
   122  		maxVersionWidth = caasMaxVersionWidth
   123  	}
   124  	truncatedWidth := maxVersionWidth - len(ellipsis)
   125  
   126  	metering := fs.Model.MeterStatus != nil
   127  	units := make(map[string]unitStatus)
   128  	var w output.Wrapper
   129  	if fs.Model.Type == caasModelType {
   130  		w = startSection(tw, false, "App", "Version", "Status", "Scale", "Charm", "Store", "Rev", "OS", "Address", "Notes")
   131  	} else {
   132  		w = startSection(tw, false, "App", "Version", "Status", "Scale", "Charm", "Store", "Rev", "OS", "Notes")
   133  	}
   134  	tw.SetColumnAlignRight(3)
   135  	tw.SetColumnAlignRight(6)
   136  	for _, appName := range naturalsort.Sort(stringKeysFromMap(fs.Applications)) {
   137  		app := fs.Applications[appName]
   138  		version := app.Version
   139  		// CAAS versions may have repo prefix we don't care about.
   140  		if fs.Model.Type == caasModelType {
   141  			parts := strings.Split(version, "/")
   142  			if len(parts) == 2 {
   143  				version = parts[1]
   144  			}
   145  		}
   146  		// Don't let a long version push out the version column.
   147  		if len(version) > maxVersionWidth {
   148  			version = version[:truncatedWidth] + ellipsis
   149  		}
   150  		// Notes may well contain other things later.
   151  		notes := ""
   152  		if app.Exposed {
   153  			notes = "exposed"
   154  		}
   155  		// Expose any operator messages.
   156  		if fs.Model.Type == caasModelType {
   157  			if app.StatusInfo.Message != "" {
   158  				notes = app.StatusInfo.Message
   159  			}
   160  		}
   161  		w.Print(appName, version)
   162  		w.PrintStatus(app.StatusInfo.Current)
   163  		scale, warn := fs.applicationScale(appName)
   164  		if warn {
   165  			w.PrintColor(output.WarningHighlight, scale)
   166  		} else {
   167  			w.Print(scale)
   168  		}
   169  
   170  		w.Print(app.CharmName,
   171  			app.CharmOrigin,
   172  			app.CharmRev,
   173  			app.OS)
   174  		if fs.Model.Type == caasModelType {
   175  			w.Print(app.Address)
   176  		}
   177  
   178  		w.Println(notes)
   179  		for un, u := range app.Units {
   180  			units[un] = u
   181  			if u.MeterStatus != nil {
   182  				metering = true
   183  			}
   184  		}
   185  	}
   186  	endSection(tw)
   187  
   188  	pUnit := func(name string, u unitStatus, level int) {
   189  		message := u.WorkloadStatusInfo.Message
   190  		// If we're still allocating and there's a message, show that.
   191  		if u.JujuStatusInfo.Current == status.Allocating && message == "" {
   192  			message = u.JujuStatusInfo.Message
   193  		}
   194  		agentDoing := agentDoing(u.JujuStatusInfo)
   195  		if agentDoing != "" {
   196  			message = fmt.Sprintf("(%s) %s", agentDoing, message)
   197  		}
   198  		if u.Leader {
   199  			name += "*"
   200  		}
   201  		w.Print(indent("", level*2, name))
   202  		w.PrintStatus(u.WorkloadStatusInfo.Current)
   203  		w.PrintStatus(u.JujuStatusInfo.Current)
   204  		if fs.Model.Type == caasModelType {
   205  			w.Println(
   206  				u.Address,
   207  				strings.Join(u.OpenedPorts, ","),
   208  				message,
   209  			)
   210  			return
   211  		}
   212  		w.Println(
   213  			u.Machine,
   214  			u.PublicAddress,
   215  			strings.Join(u.OpenedPorts, ","),
   216  			message,
   217  		)
   218  	}
   219  
   220  	if len(units) > 0 {
   221  		if fs.Model.Type == caasModelType {
   222  			startSection(tw, false, "Unit", "Workload", "Agent", "Address", "Ports", "Message")
   223  		} else {
   224  			startSection(tw, false, "Unit", "Workload", "Agent", "Machine", "Public address", "Ports", "Message")
   225  		}
   226  		for _, name := range naturalsort.Sort(stringKeysFromMap(units)) {
   227  			u := units[name]
   228  			pUnit(name, u, 0)
   229  			const indentationLevel = 1
   230  			recurseUnits(u, indentationLevel, pUnit)
   231  		}
   232  		endSection(tw)
   233  	}
   234  
   235  	if !metering {
   236  		return
   237  	}
   238  
   239  	startSection(tw, false, "Entity", "Meter status", "Message")
   240  	if fs.Model.MeterStatus != nil {
   241  		w.Print("model")
   242  		outputColor := fromMeterStatusColor(fs.Model.MeterStatus.Color)
   243  		w.PrintColor(outputColor, fs.Model.MeterStatus.Color)
   244  		w.PrintColor(outputColor, fs.Model.MeterStatus.Message)
   245  		w.Println()
   246  	}
   247  	for _, name := range naturalsort.Sort(stringKeysFromMap(units)) {
   248  		u := units[name]
   249  		if u.MeterStatus != nil {
   250  			w.Print(name)
   251  			outputColor := fromMeterStatusColor(u.MeterStatus.Color)
   252  			w.PrintColor(outputColor, u.MeterStatus.Color)
   253  			w.PrintColor(outputColor, u.MeterStatus.Message)
   254  			w.Println()
   255  		}
   256  	}
   257  	endSection(tw)
   258  }
   259  
   260  func printRemoteApplications(tw *ansiterm.TabWriter, remoteApplications map[string]remoteApplicationStatus) {
   261  	w := startSection(tw, false, "SAAS", "Status", "Store", "URL")
   262  	for _, appName := range naturalsort.Sort(stringKeysFromMap(remoteApplications)) {
   263  		app := remoteApplications[appName]
   264  		var store, urlPath string
   265  		url, err := crossmodel.ParseOfferURL(app.OfferURL)
   266  		if err == nil {
   267  			store = url.Source
   268  			url.Source = ""
   269  			urlPath = url.Path()
   270  			if store == "" {
   271  				store = "local"
   272  			}
   273  		} else {
   274  			// This is not expected.
   275  			logger.Errorf("invalid offer URL %q: %v", app.OfferURL, err)
   276  			store = "unknown"
   277  			urlPath = app.OfferURL
   278  		}
   279  		w.Print(appName)
   280  		w.PrintStatus(app.StatusInfo.Current)
   281  		w.Println(store, urlPath)
   282  	}
   283  	endSection(tw)
   284  }
   285  
   286  func printRelations(tw *ansiterm.TabWriter, relations []relationStatus) {
   287  	sort.Slice(relations, func(i, j int) bool {
   288  		a, b := relations[i], relations[j]
   289  		if a.Provider == b.Provider {
   290  			return a.Requirer < b.Requirer
   291  		}
   292  		return a.Provider < b.Provider
   293  	})
   294  
   295  	w := startSection(tw, false, "Relation provider", "Requirer", "Interface", "Type", "Message")
   296  
   297  	for _, r := range relations {
   298  		w.Print(r.Provider, r.Requirer, r.Interface, r.Type)
   299  		if r.Status != string(relation.Joined) {
   300  			w.PrintColor(cmdcrossmodel.RelationStatusColor(relation.Status(r.Status)), r.Status)
   301  			if r.Message != "" {
   302  				w.Print(" - " + r.Message)
   303  			}
   304  		}
   305  		w.Println()
   306  	}
   307  	endSection(tw)
   308  }
   309  
   310  type offerItems []offerStatus
   311  
   312  // printOffers prints a tabular summary of the offers.
   313  func printOffers(tw *ansiterm.TabWriter, offers map[string]offerStatus) error {
   314  	if len(offers) == 0 {
   315  		return nil
   316  	}
   317  	w := startSection(tw, false, "Offer", "Application", "Charm", "Rev", "Connected", "Endpoint", "Interface", "Role")
   318  	for _, offerName := range naturalsort.Sort(stringKeysFromMap(offers)) {
   319  		offer := offers[offerName]
   320  		// Sort endpoints alphabetically.
   321  		endpoints := []string{}
   322  		for endpoint := range offer.Endpoints {
   323  			endpoints = append(endpoints, endpoint)
   324  		}
   325  		sort.Strings(endpoints)
   326  
   327  		for i, endpointName := range endpoints {
   328  
   329  			endpoint := offer.Endpoints[endpointName]
   330  			if i == 0 {
   331  				// As there is some information about offer and its endpoints,
   332  				// only display offer information once when the first endpoint is displayed.
   333  				curl, err := charm.ParseURL(offer.CharmURL)
   334  				if err != nil {
   335  					return errors.Trace(err)
   336  				}
   337  				w.Println(offerName, offer.ApplicationName, curl.Name, fmt.Sprint(curl.Revision),
   338  					fmt.Sprintf("%v/%v", offer.ActiveConnectedCount, offer.TotalConnectedCount),
   339  					endpointName, endpoint.Interface, endpoint.Role)
   340  				continue
   341  			}
   342  			// Subsequent lines only need to display endpoint information.
   343  			// This will display less noise.
   344  			w.Println("", "", "", "", endpointName, endpoint.Interface, endpoint.Role)
   345  		}
   346  	}
   347  	endSection(tw)
   348  	return nil
   349  }
   350  
   351  func fromMeterStatusColor(msColor string) *ansiterm.Context {
   352  	switch msColor {
   353  	case "green":
   354  		return output.GoodHighlight
   355  	case "amber":
   356  		return output.WarningHighlight
   357  	case "red":
   358  		return output.ErrorHighlight
   359  	}
   360  	return nil
   361  }
   362  
   363  func getModelMessage(model modelStatus) string {
   364  	// Select the most important message about the model (if any).
   365  	switch {
   366  	case model.Status.Message != "":
   367  		return model.Status.Message
   368  	case model.AvailableVersion != "":
   369  		return "upgrade available: " + model.AvailableVersion
   370  	default:
   371  		return ""
   372  	}
   373  }
   374  
   375  func printMachines(tw *ansiterm.TabWriter, standAlone bool, machines map[string]machineStatus) {
   376  	w := startSection(tw, standAlone, "Machine", "State", "DNS", "Inst id", "Series", "AZ", "Message")
   377  	for _, name := range naturalsort.Sort(stringKeysFromMap(machines)) {
   378  		printMachine(w, machines[name])
   379  	}
   380  	endSection(tw)
   381  }
   382  
   383  func printMachine(w output.Wrapper, m machineStatus) {
   384  	// We want to display availability zone so extract from hardware info".
   385  	hw, err := instance.ParseHardware(m.Hardware)
   386  	if err != nil {
   387  		logger.Warningf("invalid hardware info %s for machine %v", m.Hardware, m)
   388  	}
   389  	az := ""
   390  	if hw.AvailabilityZone != nil {
   391  		az = *hw.AvailabilityZone
   392  	}
   393  
   394  	w.Print(m.Id)
   395  	w.PrintStatus(m.JujuStatus.Current)
   396  	w.Println(m.DNSName, m.machineName(), m.Series, az, m.MachineStatus.Message)
   397  	for _, name := range naturalsort.Sort(stringKeysFromMap(m.Containers)) {
   398  		printMachine(w, m.Containers[name])
   399  	}
   400  }
   401  
   402  // FormatMachineTabular writes a tabular summary of machine
   403  func FormatMachineTabular(writer io.Writer, forceColor bool, value interface{}) error {
   404  	fs, valueConverted := value.(formattedMachineStatus)
   405  	if !valueConverted {
   406  		return errors.Errorf("expected value of type %T, got %T", fs, value)
   407  	}
   408  	tw := output.TabWriter(writer)
   409  	if forceColor {
   410  		tw.SetColorCapable(forceColor)
   411  	}
   412  	printMachines(tw, true, fs.Machines)
   413  	return nil
   414  }
   415  
   416  // agentDoing returns what hook or action, if any,
   417  // the agent is currently executing.
   418  // The hook name or action is extracted from the agent message.
   419  func agentDoing(agentStatus statusInfoContents) string {
   420  	if agentStatus.Current != status.Executing {
   421  		return ""
   422  	}
   423  	// First see if we can determine a hook name.
   424  	var hookNames []string
   425  	for _, h := range hooks.UnitHooks() {
   426  		hookNames = append(hookNames, string(h))
   427  	}
   428  	for _, h := range hooks.RelationHooks() {
   429  		hookNames = append(hookNames, string(h))
   430  	}
   431  	hookExp := regexp.MustCompile(fmt.Sprintf(`running (?P<hook>%s?) hook`, strings.Join(hookNames, "|")))
   432  	match := hookExp.FindStringSubmatch(agentStatus.Message)
   433  	if len(match) > 0 {
   434  		return match[1]
   435  	}
   436  	// Now try for an action name.
   437  	actionExp := regexp.MustCompile(`running action (?P<action>.*)`)
   438  	match = actionExp.FindStringSubmatch(agentStatus.Message)
   439  	if len(match) > 0 {
   440  		return match[1]
   441  	}
   442  	return ""
   443  }