github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/cmd/juju/commands/status_formatters.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package commands
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"net"
    10  	"reflect"
    11  	"regexp"
    12  	"sort"
    13  	"strings"
    14  	"text/tabwriter"
    15  
    16  	"github.com/juju/errors"
    17  	"github.com/juju/utils/set"
    18  	"gopkg.in/juju/charm.v5/hooks"
    19  
    20  	"github.com/juju/juju/apiserver/params"
    21  )
    22  
    23  // FormatOneline returns a brief list of units and their subordinates.
    24  // Subordinates will be indented 2 spaces and listed under their
    25  // superiors.
    26  func FormatOneline(value interface{}) ([]byte, error) {
    27  	fs, valueConverted := value.(formattedStatus)
    28  	if !valueConverted {
    29  		return nil, errors.Errorf("expected value of type %T, got %T", fs, value)
    30  	}
    31  	var out bytes.Buffer
    32  
    33  	pprint := func(uName string, u unitStatus, level int) {
    34  		var fmtPorts string
    35  		if len(u.OpenedPorts) > 0 {
    36  			fmtPorts = fmt.Sprintf(" %s", strings.Join(u.OpenedPorts, ", "))
    37  		}
    38  		fmt.Fprintf(&out, indent("\n", level*2, "- %s: %s (%v)%v"),
    39  			uName,
    40  			u.PublicAddress,
    41  			u.AgentState,
    42  			fmtPorts,
    43  		)
    44  	}
    45  
    46  	for _, svcName := range sortStringsNaturally(stringKeysFromMap(fs.Services)) {
    47  		svc := fs.Services[svcName]
    48  		for _, uName := range sortStringsNaturally(stringKeysFromMap(svc.Units)) {
    49  			unit := svc.Units[uName]
    50  			pprint(uName, unit, 0)
    51  			recurseUnits(unit, 1, pprint)
    52  		}
    53  	}
    54  
    55  	return out.Bytes(), nil
    56  }
    57  
    58  // agentDoing returns what hook or action, if any,
    59  // the agent is currently executing.
    60  // The hook name or action is extracted from the agent message.
    61  func agentDoing(status statusInfoContents) string {
    62  	if status.Current != params.StatusExecuting {
    63  		return ""
    64  	}
    65  	// First see if we can determine a hook name.
    66  	var hookNames []string
    67  	for _, h := range hooks.UnitHooks() {
    68  		hookNames = append(hookNames, string(h))
    69  	}
    70  	for _, h := range hooks.RelationHooks() {
    71  		hookNames = append(hookNames, string(h))
    72  	}
    73  	hookExp := regexp.MustCompile(fmt.Sprintf(`running (?P<hook>%s?) hook`, strings.Join(hookNames, "|")))
    74  	match := hookExp.FindStringSubmatch(status.Message)
    75  	if len(match) > 0 {
    76  		return match[1]
    77  	}
    78  	// Now try for an action name.
    79  	actionExp := regexp.MustCompile(`running action (?P<action>.*)`)
    80  	match = actionExp.FindStringSubmatch(status.Message)
    81  	if len(match) > 0 {
    82  		return match[1]
    83  	}
    84  	return ""
    85  }
    86  
    87  // FormatTabular returns a tabular summary of machines, services, and
    88  // units. Any subordinate items are indented by two spaces beneath
    89  // their superior.
    90  func FormatTabular(value interface{}) ([]byte, error) {
    91  	fs, valueConverted := value.(formattedStatus)
    92  	if !valueConverted {
    93  		return nil, errors.Errorf("expected value of type %T, got %T", fs, value)
    94  	}
    95  	var out bytes.Buffer
    96  	// To format things into columns.
    97  	tw := tabwriter.NewWriter(&out, 0, 1, 1, ' ', 0)
    98  	p := func(values ...interface{}) {
    99  		for _, v := range values {
   100  			fmt.Fprintf(tw, "%s\t", v)
   101  		}
   102  		fmt.Fprintln(tw)
   103  	}
   104  
   105  	units := make(map[string]unitStatus)
   106  	p("[Services]")
   107  	p("NAME\tSTATUS\tEXPOSED\tCHARM")
   108  	for _, svcName := range sortStringsNaturally(stringKeysFromMap(fs.Services)) {
   109  		svc := fs.Services[svcName]
   110  		for un, u := range svc.Units {
   111  			units[un] = u
   112  		}
   113  		p(svcName, svc.StatusInfo.Current, fmt.Sprintf("%t", svc.Exposed), svc.Charm)
   114  	}
   115  	tw.Flush()
   116  
   117  	pUnit := func(name string, u unitStatus, level int) {
   118  		message := u.WorkloadStatusInfo.Message
   119  		agentDoing := agentDoing(u.AgentStatusInfo)
   120  		if agentDoing != "" {
   121  			message = fmt.Sprintf("(%s) %s", agentDoing, message)
   122  		}
   123  		p(
   124  			indent("", level*2, name),
   125  			u.WorkloadStatusInfo.Current,
   126  			u.AgentStatusInfo.Current,
   127  			u.AgentStatusInfo.Version,
   128  			u.Machine,
   129  			strings.Join(u.OpenedPorts, ","),
   130  			u.PublicAddress,
   131  			message,
   132  		)
   133  	}
   134  
   135  	// See if we have new or old data; that determines what data we can display.
   136  	newStatus := false
   137  	for _, u := range units {
   138  		if u.AgentStatusInfo.Current != "" {
   139  			newStatus = true
   140  			break
   141  		}
   142  	}
   143  	var header []string
   144  	if newStatus {
   145  		header = []string{"ID", "WORKLOAD-STATE", "AGENT-STATE", "VERSION", "MACHINE", "PORTS", "PUBLIC-ADDRESS", "MESSAGE"}
   146  	} else {
   147  		header = []string{"ID", "STATE", "VERSION", "MACHINE", "PORTS", "PUBLIC-ADDRESS"}
   148  	}
   149  
   150  	p("\n[Units]")
   151  	p(strings.Join(header, "\t"))
   152  	for _, name := range sortStringsNaturally(stringKeysFromMap(units)) {
   153  		u := units[name]
   154  		pUnit(name, u, 0)
   155  		const indentationLevel = 1
   156  		recurseUnits(u, indentationLevel, pUnit)
   157  	}
   158  	tw.Flush()
   159  
   160  	p("\n[Machines]")
   161  	p("ID\tSTATE\tVERSION\tDNS\tINS-ID\tSERIES\tHARDWARE")
   162  	for _, name := range sortStringsNaturally(stringKeysFromMap(fs.Machines)) {
   163  		m := fs.Machines[name]
   164  		p(m.Id, m.AgentState, m.AgentVersion, m.DNSName, m.InstanceId, m.Series, m.Hardware)
   165  	}
   166  	tw.Flush()
   167  
   168  	return out.Bytes(), nil
   169  }
   170  
   171  // FormatSummary returns a summary of the current environment
   172  // including the following information:
   173  // - Headers:
   174  //   - All subnets the environment occupies.
   175  //   - All ports the environment utilizes.
   176  // - Sections:
   177  //   - Machines: Displays total #, and then the # in each state.
   178  //   - Units: Displays total #, and then # in each state.
   179  //   - Services: Displays total #, their names, and how many of each
   180  //     are exposed.
   181  func FormatSummary(value interface{}) ([]byte, error) {
   182  	fs, valueConverted := value.(formattedStatus)
   183  	if !valueConverted {
   184  		return nil, errors.Errorf("expected value of type %T, got %T", fs, value)
   185  	}
   186  
   187  	f := newSummaryFormatter()
   188  	stateToMachine := f.aggregateMachineStates(fs.Machines)
   189  	svcExposure := f.aggregateServiceAndUnitStates(fs.Services)
   190  	p := f.delimitValuesWithTabs
   191  
   192  	// Print everything out
   193  	p("Running on subnets:", strings.Join(f.netStrings, ", "))
   194  	p("Utilizing ports:", f.portsInColumnsOf(3))
   195  	f.tw.Flush()
   196  
   197  	// Right align summary information
   198  	f.tw.Init(&f.out, 0, 2, 1, ' ', tabwriter.AlignRight)
   199  	p("# MACHINES:", fmt.Sprintf("(%d)", len(fs.Machines)))
   200  	f.printStateToCount(stateToMachine)
   201  	p(" ")
   202  
   203  	p("# UNITS:", fmt.Sprintf("(%d)", f.numUnits))
   204  	f.printStateToCount(f.stateToUnit)
   205  	p(" ")
   206  
   207  	p("# SERVICES:", fmt.Sprintf(" (%d)", len(fs.Services)))
   208  	for _, svcName := range sortStringsNaturally(stringKeysFromMap(svcExposure)) {
   209  		s := svcExposure[svcName]
   210  		p(svcName, fmt.Sprintf("%d/%d\texposed", s[true], s[true]+s[false]))
   211  	}
   212  	f.tw.Flush()
   213  
   214  	return f.out.Bytes(), nil
   215  }
   216  
   217  func newSummaryFormatter() *summaryFormatter {
   218  	f := &summaryFormatter{
   219  		ipAddrs:     make([]net.IPNet, 0),
   220  		netStrings:  make([]string, 0),
   221  		openPorts:   set.NewStrings(),
   222  		stateToUnit: make(map[params.Status]int),
   223  	}
   224  	f.tw = tabwriter.NewWriter(&f.out, 0, 1, 1, ' ', 0)
   225  	return f
   226  }
   227  
   228  type summaryFormatter struct {
   229  	ipAddrs    []net.IPNet
   230  	netStrings []string
   231  	numUnits   int
   232  	openPorts  set.Strings
   233  	// status -> count
   234  	stateToUnit map[params.Status]int
   235  	tw          *tabwriter.Writer
   236  	out         bytes.Buffer
   237  }
   238  
   239  func (f *summaryFormatter) delimitValuesWithTabs(values ...string) {
   240  	for _, v := range values {
   241  		fmt.Fprintf(f.tw, "%s\t", v)
   242  	}
   243  	fmt.Fprintln(f.tw)
   244  }
   245  
   246  func (f *summaryFormatter) portsInColumnsOf(col int) string {
   247  
   248  	var b bytes.Buffer
   249  	for i, p := range f.openPorts.SortedValues() {
   250  		if i != 0 && i%col == 0 {
   251  			fmt.Fprintf(&b, "\n\t")
   252  		}
   253  		fmt.Fprintf(&b, "%s, ", p)
   254  	}
   255  	// Elide the last delimiter
   256  	portList := b.String()
   257  	if len(portList) >= 2 {
   258  		return portList[:b.Len()-2]
   259  	}
   260  	return portList
   261  }
   262  
   263  func (f *summaryFormatter) trackUnit(name string, status unitStatus, indentLevel int) {
   264  	f.resolveAndTrackIp(status.PublicAddress)
   265  
   266  	for _, p := range status.OpenedPorts {
   267  		if p != "" {
   268  			f.openPorts.Add(p)
   269  		}
   270  	}
   271  	f.numUnits++
   272  	f.stateToUnit[status.AgentState]++
   273  }
   274  
   275  func (f *summaryFormatter) printStateToCount(m map[params.Status]int) {
   276  	for _, status := range sortStringsNaturally(stringKeysFromMap(m)) {
   277  		numInStatus := m[params.Status(status)]
   278  		f.delimitValuesWithTabs(status+":", fmt.Sprintf(" %d ", numInStatus))
   279  	}
   280  }
   281  
   282  func (f *summaryFormatter) trackIp(ip net.IP) {
   283  	for _, net := range f.ipAddrs {
   284  		if net.Contains(ip) {
   285  			return
   286  		}
   287  	}
   288  
   289  	ipNet := net.IPNet{ip, ip.DefaultMask()}
   290  	f.ipAddrs = append(f.ipAddrs, ipNet)
   291  	f.netStrings = append(f.netStrings, ipNet.String())
   292  }
   293  
   294  func (f *summaryFormatter) resolveAndTrackIp(publicDns string) {
   295  	// TODO(katco-): We may be able to utilize upcoming work which will expose these addresses outright.
   296  	ip, err := net.ResolveIPAddr("ip4", publicDns)
   297  	if err != nil {
   298  		logger.Warningf(
   299  			"unable to resolve %s to an IP address. Status may be incorrect: %v",
   300  			publicDns,
   301  			err,
   302  		)
   303  		return
   304  	}
   305  	f.trackIp(ip.IP)
   306  }
   307  
   308  func (f *summaryFormatter) aggregateMachineStates(machines map[string]machineStatus) map[params.Status]int {
   309  	stateToMachine := make(map[params.Status]int)
   310  	for _, name := range sortStringsNaturally(stringKeysFromMap(machines)) {
   311  		m := machines[name]
   312  		f.resolveAndTrackIp(m.DNSName)
   313  
   314  		if agentState := m.AgentState; agentState == "" {
   315  			agentState = params.StatusPending
   316  		} else {
   317  			stateToMachine[agentState]++
   318  		}
   319  	}
   320  	return stateToMachine
   321  }
   322  
   323  func (f *summaryFormatter) aggregateServiceAndUnitStates(services map[string]serviceStatus) map[string]map[bool]int {
   324  	svcExposure := make(map[string]map[bool]int)
   325  	for _, name := range sortStringsNaturally(stringKeysFromMap(services)) {
   326  		s := services[name]
   327  		// Grab unit states
   328  		for _, un := range sortStringsNaturally(stringKeysFromMap(s.Units)) {
   329  			u := s.Units[un]
   330  			f.trackUnit(un, u, 0)
   331  			recurseUnits(u, 1, f.trackUnit)
   332  		}
   333  
   334  		if _, ok := svcExposure[name]; !ok {
   335  			svcExposure[name] = make(map[bool]int)
   336  		}
   337  
   338  		svcExposure[name][s.Exposed]++
   339  	}
   340  	return svcExposure
   341  }
   342  
   343  // sortStringsNaturally is syntactic sugar so we can do sorts in one line.
   344  func sortStringsNaturally(s []string) []string {
   345  	sort.Sort(naturally(s))
   346  	return s
   347  }
   348  
   349  // stringKeysFromMap takes a map with keys which are strings and returns
   350  // only the keys.
   351  func stringKeysFromMap(m interface{}) (keys []string) {
   352  	for _, k := range reflect.ValueOf(m).MapKeys() {
   353  		keys = append(keys, k.String())
   354  	}
   355  	return
   356  }
   357  
   358  // recurseUnits calls the given recurseMap function on the given unit
   359  // and its subordinates (recursively defined on the given unit).
   360  func recurseUnits(u unitStatus, il int, recurseMap func(string, unitStatus, int)) {
   361  	if len(u.Subordinates) == 0 {
   362  		return
   363  	}
   364  	for _, uName := range sortStringsNaturally(stringKeysFromMap(u.Subordinates)) {
   365  		unit := u.Subordinates[uName]
   366  		recurseMap(uName, unit, il)
   367  		recurseUnits(unit, il+1, recurseMap)
   368  	}
   369  }
   370  
   371  // indent prepends a format string with the given number of spaces.
   372  func indent(prepend string, level int, append string) string {
   373  	return fmt.Sprintf("%s%*s%s", prepend, level, "", append)
   374  }