github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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/utils"
    16  	"github.com/juju/utils/set"
    17  	"gopkg.in/juju/charm.v6-unstable/hooks"
    18  
    19  	"github.com/juju/juju/cmd/output"
    20  	"github.com/juju/juju/instance"
    21  	"github.com/juju/juju/status"
    22  )
    23  
    24  type statusRelation struct {
    25  	application1 string
    26  	application2 string
    27  	relation     string
    28  	subordinate  bool
    29  }
    30  
    31  func (s *statusRelation) relationType() string {
    32  	if s.subordinate {
    33  		return "subordinate"
    34  	} else if s.application1 == s.application2 {
    35  		return "peer"
    36  	}
    37  	return "regular"
    38  }
    39  
    40  type relationFormatter struct {
    41  	relationIndex set.Strings
    42  	relations     map[string]*statusRelation
    43  }
    44  
    45  func newRelationFormatter() *relationFormatter {
    46  	return &relationFormatter{
    47  		relationIndex: set.NewStrings(),
    48  		relations:     make(map[string]*statusRelation),
    49  	}
    50  }
    51  
    52  func (r *relationFormatter) len() int {
    53  	return r.relationIndex.Size()
    54  }
    55  
    56  func (r *relationFormatter) add(rel1, rel2, relation string, is2SubOf1 bool) {
    57  	rel := []string{rel1, rel2}
    58  	if !is2SubOf1 {
    59  		sort.Sort(sort.StringSlice(rel))
    60  	}
    61  	k := strings.Join(rel, "\t")
    62  	r.relations[k] = &statusRelation{
    63  		application1: rel[0],
    64  		application2: rel[1],
    65  		relation:     relation,
    66  		subordinate:  is2SubOf1,
    67  	}
    68  	r.relationIndex.Add(k)
    69  }
    70  
    71  func (r *relationFormatter) sorted() []string {
    72  	return r.relationIndex.SortedValues()
    73  }
    74  
    75  func (r *relationFormatter) get(k string) *statusRelation {
    76  	return r.relations[k]
    77  }
    78  
    79  // FormatTabular writes a tabular summary of machines, applications, and
    80  // units. Any subordinate items are indented by two spaces beneath
    81  // their superior.
    82  func FormatTabular(writer io.Writer, forceColor bool, value interface{}) error {
    83  	const maxVersionWidth = 15
    84  	const ellipsis = "..."
    85  	const truncatedWidth = maxVersionWidth - len(ellipsis)
    86  
    87  	fs, valueConverted := value.(formattedStatus)
    88  	if !valueConverted {
    89  		return errors.Errorf("expected value of type %T, got %T", fs, value)
    90  	}
    91  	// To format things into columns.
    92  	tw := output.TabWriter(writer)
    93  	if forceColor {
    94  		tw.SetColorCapable(forceColor)
    95  	}
    96  	w := output.Wrapper{tw}
    97  	p := w.Println
    98  	outputHeaders := func(values ...interface{}) {
    99  		p()
   100  		p(values...)
   101  	}
   102  
   103  	cloudRegion := fs.Model.Cloud
   104  	if fs.Model.CloudRegion != "" {
   105  		cloudRegion += "/" + fs.Model.CloudRegion
   106  	}
   107  
   108  	header := []interface{}{"MODEL", "CONTROLLER", "CLOUD/REGION", "VERSION"}
   109  	values := []interface{}{fs.Model.Name, fs.Model.Controller, cloudRegion, fs.Model.Version}
   110  	message := getModelMessage(fs.Model)
   111  	if message != "" {
   112  		header = append(header, "NOTES")
   113  		values = append(values, message)
   114  	}
   115  
   116  	// The first set of headers don't use outputHeaders because it adds the blank line.
   117  	p(header...)
   118  	p(values...)
   119  
   120  	units := make(map[string]unitStatus)
   121  	metering := false
   122  	relations := newRelationFormatter()
   123  	outputHeaders("APP", "VERSION", "STATUS", "SCALE", "CHARM", "STORE", "REV", "OS", "NOTES")
   124  	tw.SetColumnAlignRight(3)
   125  	tw.SetColumnAlignRight(6)
   126  	for _, appName := range utils.SortStringsNaturally(stringKeysFromMap(fs.Applications)) {
   127  		app := fs.Applications[appName]
   128  		version := app.Version
   129  		// Don't let a long version push out the version column.
   130  		if len(version) > maxVersionWidth {
   131  			version = version[:truncatedWidth] + ellipsis
   132  		}
   133  		// Notes may well contain other things later.
   134  		notes := ""
   135  		if app.Exposed {
   136  			notes = "exposed"
   137  		}
   138  		w.Print(appName, version)
   139  		w.PrintStatus(app.StatusInfo.Current)
   140  		scale, warn := fs.applicationScale(appName)
   141  		if warn {
   142  			w.PrintColor(output.WarningHighlight, scale)
   143  		} else {
   144  			w.Print(scale)
   145  		}
   146  		p(app.CharmName,
   147  			app.CharmOrigin,
   148  			app.CharmRev,
   149  			app.OS,
   150  			notes)
   151  
   152  		for un, u := range app.Units {
   153  			units[un] = u
   154  			if u.MeterStatus != nil {
   155  				metering = true
   156  			}
   157  		}
   158  		// Ensure that we pick a consistent name for peer relations.
   159  		sortedRelTypes := make([]string, 0, len(app.Relations))
   160  		for relType := range app.Relations {
   161  			sortedRelTypes = append(sortedRelTypes, relType)
   162  		}
   163  		sort.Strings(sortedRelTypes)
   164  
   165  		subs := set.NewStrings(app.SubordinateTo...)
   166  		for _, relType := range sortedRelTypes {
   167  			for _, related := range app.Relations[relType] {
   168  				relations.add(related, appName, relType, subs.Contains(related))
   169  			}
   170  		}
   171  
   172  	}
   173  
   174  	pUnit := func(name string, u unitStatus, level int) {
   175  		message := u.WorkloadStatusInfo.Message
   176  		agentDoing := agentDoing(u.JujuStatusInfo)
   177  		if agentDoing != "" {
   178  			message = fmt.Sprintf("(%s) %s", agentDoing, message)
   179  		}
   180  		if u.Leader {
   181  			name += "*"
   182  		}
   183  		w.Print(indent("", level*2, name))
   184  		w.PrintStatus(u.WorkloadStatusInfo.Current)
   185  		w.PrintStatus(u.JujuStatusInfo.Current)
   186  		p(
   187  			u.Machine,
   188  			u.PublicAddress,
   189  			strings.Join(u.OpenedPorts, ","),
   190  			message,
   191  		)
   192  	}
   193  
   194  	outputHeaders("UNIT", "WORKLOAD", "AGENT", "MACHINE", "PUBLIC-ADDRESS", "PORTS", "MESSAGE")
   195  	for _, name := range utils.SortStringsNaturally(stringKeysFromMap(units)) {
   196  		u := units[name]
   197  		pUnit(name, u, 0)
   198  		const indentationLevel = 1
   199  		recurseUnits(u, indentationLevel, pUnit)
   200  	}
   201  
   202  	if metering {
   203  		outputHeaders("METER", "STATUS", "MESSAGE")
   204  		for _, name := range utils.SortStringsNaturally(stringKeysFromMap(units)) {
   205  			u := units[name]
   206  			if u.MeterStatus != nil {
   207  				p(name, u.MeterStatus.Color, u.MeterStatus.Message)
   208  			}
   209  		}
   210  	}
   211  
   212  	p()
   213  	printMachines(tw, fs.Machines)
   214  
   215  	if relations.len() > 0 {
   216  		outputHeaders("RELATION", "PROVIDES", "CONSUMES", "TYPE")
   217  		for _, k := range relations.sorted() {
   218  			r := relations.get(k)
   219  			if r != nil {
   220  				p(r.relation, r.application1, r.application2, r.relationType())
   221  			}
   222  		}
   223  	}
   224  
   225  	tw.Flush()
   226  	return nil
   227  }
   228  
   229  func getModelMessage(model modelStatus) string {
   230  	// Select the most important message about the model (if any).
   231  	switch {
   232  	case model.Migration != "":
   233  		return "migrating: " + model.Migration
   234  	case model.AvailableVersion != "":
   235  		return "upgrade available: " + model.AvailableVersion
   236  	default:
   237  		return ""
   238  	}
   239  }
   240  
   241  func printMachines(tw *ansiterm.TabWriter, machines map[string]machineStatus) {
   242  	w := output.Wrapper{tw}
   243  	w.Println("MACHINE", "STATE", "DNS", "INS-ID", "SERIES", "AZ")
   244  	for _, name := range utils.SortStringsNaturally(stringKeysFromMap(machines)) {
   245  		printMachine(w, machines[name])
   246  	}
   247  }
   248  
   249  func printMachine(w output.Wrapper, m machineStatus) {
   250  	// We want to display availability zone so extract from hardware info".
   251  	hw, err := instance.ParseHardware(m.Hardware)
   252  	if err != nil {
   253  		logger.Warningf("invalid hardware info %s for machine %v", m.Hardware, m)
   254  	}
   255  	az := ""
   256  	if hw.AvailabilityZone != nil {
   257  		az = *hw.AvailabilityZone
   258  	}
   259  	w.Print(m.Id)
   260  	w.PrintStatus(m.JujuStatus.Current)
   261  	w.Println(m.DNSName, m.InstanceId, m.Series, az)
   262  	for _, name := range utils.SortStringsNaturally(stringKeysFromMap(m.Containers)) {
   263  		printMachine(w, m.Containers[name])
   264  	}
   265  }
   266  
   267  // FormatMachineTabular writes a tabular summary of machine
   268  func FormatMachineTabular(writer io.Writer, forceColor bool, value interface{}) error {
   269  	fs, valueConverted := value.(formattedMachineStatus)
   270  	if !valueConverted {
   271  		return errors.Errorf("expected value of type %T, got %T", fs, value)
   272  	}
   273  	tw := output.TabWriter(writer)
   274  	if forceColor {
   275  		tw.SetColorCapable(forceColor)
   276  	}
   277  	printMachines(tw, fs.Machines)
   278  	tw.Flush()
   279  
   280  	return nil
   281  }
   282  
   283  // agentDoing returns what hook or action, if any,
   284  // the agent is currently executing.
   285  // The hook name or action is extracted from the agent message.
   286  func agentDoing(agentStatus statusInfoContents) string {
   287  	if agentStatus.Current != status.Executing {
   288  		return ""
   289  	}
   290  	// First see if we can determine a hook name.
   291  	var hookNames []string
   292  	for _, h := range hooks.UnitHooks() {
   293  		hookNames = append(hookNames, string(h))
   294  	}
   295  	for _, h := range hooks.RelationHooks() {
   296  		hookNames = append(hookNames, string(h))
   297  	}
   298  	hookExp := regexp.MustCompile(fmt.Sprintf(`running (?P<hook>%s?) hook`, strings.Join(hookNames, "|")))
   299  	match := hookExp.FindStringSubmatch(agentStatus.Message)
   300  	if len(match) > 0 {
   301  		return match[1]
   302  	}
   303  	// Now try for an action name.
   304  	actionExp := regexp.MustCompile(`running action (?P<action>.*)`)
   305  	match = actionExp.FindStringSubmatch(agentStatus.Message)
   306  	if len(match) > 0 {
   307  		return match[1]
   308  	}
   309  	return ""
   310  }