github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/status/history.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  	"os"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/juju/cmd"
    15  	"github.com/juju/collections/set"
    16  	"github.com/juju/errors"
    17  	"github.com/juju/gnuflag"
    18  	"gopkg.in/juju/names.v2"
    19  
    20  	jujucmd "github.com/juju/juju/cmd"
    21  	"github.com/juju/juju/cmd/juju/common"
    22  	"github.com/juju/juju/cmd/modelcmd"
    23  	"github.com/juju/juju/cmd/output"
    24  	"github.com/juju/juju/core/status"
    25  	"github.com/juju/juju/juju/osenv"
    26  )
    27  
    28  // TODO(peritto666) - add tests
    29  
    30  // NewStatusHistoryCommand returns a command that reports the history
    31  // of status changes for the specified unit.
    32  func NewStatusHistoryCommand() cmd.Command {
    33  	return modelcmd.Wrap(&statusHistoryCommand{})
    34  }
    35  
    36  // HistoryAPI is the API surface for the show-status-log command.
    37  type HistoryAPI interface {
    38  	StatusHistory(kind status.HistoryKind, tag names.Tag, filter status.StatusHistoryFilter) (status.History, error)
    39  	Close() error
    40  }
    41  
    42  type statusHistoryCommand struct {
    43  	modelcmd.ModelCommandBase
    44  	api                  HistoryAPI
    45  	out                  cmd.Output
    46  	outputContent        string
    47  	backlogSize          int
    48  	backlogSizeDays      int
    49  	backlogDate          string
    50  	isoTime              bool
    51  	entityName           string
    52  	date                 time.Time
    53  	includeStatusUpdates bool
    54  }
    55  
    56  var statusHistoryDoc = fmt.Sprintf(`
    57  This command will report the history of status changes for
    58  a given entity.
    59  The statuses are available for the following types.
    60  -type supports:
    61  %v
    62   and sorted by time of occurrence.
    63   The default is unit.
    64  `, supportedHistoryKindDescs())
    65  
    66  func (c *statusHistoryCommand) Info() *cmd.Info {
    67  	return jujucmd.Info(&cmd.Info{
    68  		Name:    "show-status-log",
    69  		Args:    "<entity name>",
    70  		Purpose: "Output past statuses for the specified entity.",
    71  		Doc:     statusHistoryDoc,
    72  	})
    73  }
    74  
    75  func supportedHistoryKindTypes() string {
    76  	supported := set.NewStrings()
    77  	for k := range status.AllHistoryKind() {
    78  		supported.Add(string(k))
    79  	}
    80  	return strings.Join(supported.SortedValues(), "|")
    81  }
    82  
    83  func supportedHistoryKindDescs() string {
    84  	types := status.AllHistoryKind()
    85  	supported := set.NewStrings()
    86  	for k := range types {
    87  		supported.Add(string(k))
    88  	}
    89  	all := ""
    90  	for _, k := range supported.SortedValues() {
    91  		all += fmt.Sprintf("    %v:  %v\n", k, types[status.HistoryKind(k)])
    92  	}
    93  	return all
    94  }
    95  
    96  func (c *statusHistoryCommand) SetFlags(f *gnuflag.FlagSet) {
    97  	c.ModelCommandBase.SetFlags(f)
    98  	f.StringVar(&c.outputContent, "type", "unit", fmt.Sprintf("Type of statuses to be displayed [%v]", supportedHistoryKindTypes()))
    99  	f.IntVar(&c.backlogSize, "n", 0, "Returns the last N logs (cannot be combined with --days or --date)")
   100  	f.IntVar(&c.backlogSizeDays, "days", 0, "Returns the logs for the past <days> days (cannot be combined with -n or --date)")
   101  	f.StringVar(&c.backlogDate, "from-date", "", "Returns logs for any date after the passed one, the expected date format is YYYY-MM-DD (cannot be combined with -n or --days)")
   102  	f.BoolVar(&c.isoTime, "utc", false, "Display time as UTC in RFC3339 format")
   103  	// TODO (anastasiamac 2018-04-11) Remove at the next major release, say Juju 2.5+ or Juju 3.x.
   104  	// the functionality is no longer there since a fix for lp#1530840
   105  	f.BoolVar(&c.includeStatusUpdates, "include-status-updates", false, "Deprecated, has no effect for 2.3+ controllers: Include update status hook messages in the returned logs")
   106  }
   107  
   108  func (c *statusHistoryCommand) Init(args []string) error {
   109  	switch {
   110  	case len(args) > 1:
   111  		return errors.Errorf("unexpected arguments after entity name.")
   112  	case len(args) == 0:
   113  		return errors.Errorf("entity name is missing.")
   114  	default:
   115  		c.entityName = args[0]
   116  	}
   117  	// If use of ISO time not specified on command line,
   118  	// check env var.
   119  	if !c.isoTime {
   120  		var err error
   121  		envVarValue := os.Getenv(osenv.JujuStatusIsoTimeEnvKey)
   122  		if envVarValue != "" {
   123  			if c.isoTime, err = strconv.ParseBool(envVarValue); err != nil {
   124  				return errors.Annotatef(err, "invalid %s env var, expected true|false", osenv.JujuStatusIsoTimeEnvKey)
   125  			}
   126  		}
   127  	}
   128  	emptyDate := c.backlogDate == ""
   129  	emptySize := c.backlogSize == 0
   130  	emptyDays := c.backlogSizeDays == 0
   131  	if emptyDate && emptySize && emptyDays {
   132  		c.backlogSize = 20
   133  	}
   134  	if (!emptyDays && !emptySize) || (!emptyDays && !emptyDate) || (!emptySize && !emptyDate) {
   135  		return errors.Errorf("backlog size, backlog date and backlog days back cannot be specified together")
   136  	}
   137  	if c.backlogDate != "" {
   138  		var err error
   139  		c.date, err = time.Parse("2006-01-02", c.backlogDate)
   140  		if err != nil {
   141  			return errors.Annotate(err, "parsing backlog date")
   142  		}
   143  	}
   144  
   145  	kind := status.HistoryKind(c.outputContent)
   146  	if kind.Valid() {
   147  		return nil
   148  	}
   149  	return errors.Errorf("unexpected status type %q", c.outputContent)
   150  }
   151  
   152  const runningHookMSG = "running update-status hook"
   153  
   154  func (c *statusHistoryCommand) getAPI() (HistoryAPI, error) {
   155  	if c.api != nil {
   156  		return c.api, nil
   157  	}
   158  	return c.NewAPIClient()
   159  }
   160  
   161  func (c *statusHistoryCommand) Run(ctx *cmd.Context) error {
   162  	apiclient, err := c.getAPI()
   163  	if err != nil {
   164  		return errors.Trace(err)
   165  	}
   166  	defer apiclient.Close()
   167  	kind := status.HistoryKind(c.outputContent)
   168  	var delta *time.Duration
   169  
   170  	if c.backlogSizeDays != 0 {
   171  		t := time.Duration(c.backlogSizeDays*24) * time.Hour
   172  		delta = &t
   173  	}
   174  	filterArgs := status.StatusHistoryFilter{
   175  		Size:  c.backlogSize,
   176  		Delta: delta,
   177  	}
   178  	if !c.includeStatusUpdates {
   179  		filterArgs.Exclude = set.NewStrings(runningHookMSG)
   180  	}
   181  
   182  	if !c.date.IsZero() {
   183  		filterArgs.FromDate = &c.date
   184  	}
   185  	var tag names.Tag
   186  	switch kind {
   187  	case status.KindUnit, status.KindWorkload, status.KindUnitAgent:
   188  		if !names.IsValidUnit(c.entityName) {
   189  			return errors.Errorf("%q is not a valid name for a %s", c.entityName, kind)
   190  		}
   191  		tag = names.NewUnitTag(c.entityName)
   192  	default:
   193  		if !names.IsValidMachine(c.entityName) {
   194  			return errors.Errorf("%q is not a valid name for a %s", c.entityName, kind)
   195  		}
   196  		tag = names.NewMachineTag(c.entityName)
   197  	}
   198  	statuses, err := apiclient.StatusHistory(kind, tag, filterArgs)
   199  	historyLen := len(statuses)
   200  	if err != nil {
   201  		if historyLen == 0 {
   202  			return errors.Trace(err)
   203  		}
   204  		// Display any error, but continue to print status if some was returned
   205  		fmt.Fprintf(ctx.Stderr, "%v\n", err)
   206  	}
   207  
   208  	if historyLen == 0 {
   209  		return errors.Errorf("no status history available")
   210  	}
   211  
   212  	c.writeTabular(ctx.Stdout, statuses)
   213  	return nil
   214  }
   215  
   216  func (c *statusHistoryCommand) writeTabular(writer io.Writer, statuses status.History) {
   217  	tw := output.TabWriter(writer)
   218  	w := output.Wrapper{tw}
   219  
   220  	w.Println("Time", "Type", "Status", "Message")
   221  	for _, v := range statuses {
   222  		w.Print(common.FormatTime(v.Since, c.isoTime), v.Kind)
   223  		w.PrintStatus(v.Status)
   224  		w.Println(v.Info)
   225  	}
   226  	tw.Flush()
   227  }