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 }