github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/juju/action/showoutput.go (about) 1 // Copyright 2014, 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package action 5 6 import ( 7 "regexp" 8 "time" 9 10 "github.com/juju/cmd" 11 errors "github.com/juju/errors" 12 "github.com/juju/gnuflag" 13 14 "github.com/juju/juju/apiserver/params" 15 "github.com/juju/juju/cmd/modelcmd" 16 "github.com/juju/juju/cmd/output" 17 ) 18 19 func NewShowOutputCommand() cmd.Command { 20 return modelcmd.Wrap(&showOutputCommand{}) 21 } 22 23 // showOutputCommand fetches the results of an action by ID. 24 type showOutputCommand struct { 25 ActionCommandBase 26 out cmd.Output 27 requestedId string 28 fullSchema bool 29 wait string 30 } 31 32 const showOutputDoc = ` 33 Show the results returned by an action with the given ID. A partial ID may 34 also be used. To block until the result is known completed or failed, use 35 the --wait flag with a duration, as in --wait 5s or --wait 1h. Use --wait 0 36 to wait indefinitely. If units are left off, seconds are assumed. 37 38 The default behavior without --wait is to immediately check and return; if 39 the results are "pending" then only the available information will be 40 displayed. This is also the behavior when any negative time is given. 41 ` 42 43 // Set up the output. 44 func (c *showOutputCommand) SetFlags(f *gnuflag.FlagSet) { 45 c.ActionCommandBase.SetFlags(f) 46 c.out.AddFlags(f, "yaml", output.DefaultFormatters) 47 f.StringVar(&c.wait, "wait", "-1s", "Wait for results") 48 } 49 50 func (c *showOutputCommand) Info() *cmd.Info { 51 return &cmd.Info{ 52 Name: "show-action-output", 53 Args: "<action ID>", 54 Purpose: "Show results of an action by ID.", 55 Doc: showOutputDoc, 56 } 57 } 58 59 // Init validates the action ID and any other options. 60 func (c *showOutputCommand) Init(args []string) error { 61 switch len(args) { 62 case 0: 63 return errors.New("no action ID specified") 64 case 1: 65 c.requestedId = args[0] 66 return nil 67 default: 68 return cmd.CheckEmpty(args[1:]) 69 } 70 } 71 72 // Run issues the API call to get Actions by ID. 73 func (c *showOutputCommand) Run(ctx *cmd.Context) error { 74 // Check whether units were left off our time string. 75 r := regexp.MustCompile("[a-zA-Z]") 76 matches := r.FindStringSubmatch(c.wait[len(c.wait)-1:]) 77 // If any match, we have units. Otherwise, we don't; assume seconds. 78 if len(matches) == 0 { 79 c.wait = c.wait + "s" 80 } 81 82 waitDur, err := time.ParseDuration(c.wait) 83 if err != nil { 84 return err 85 } 86 87 api, err := c.NewActionAPIClient() 88 if err != nil { 89 return err 90 } 91 defer api.Close() 92 93 wait := time.NewTimer(0 * time.Second) 94 95 switch { 96 case waitDur.Nanoseconds() < 0: 97 // Negative duration signals immediate return. All is well. 98 case waitDur.Nanoseconds() == 0: 99 // Zero duration signals indefinite wait. Discard the tick. 100 wait = time.NewTimer(0 * time.Second) 101 _ = <-wait.C 102 default: 103 // Otherwise, start an ordinary timer. 104 wait = time.NewTimer(waitDur) 105 } 106 107 result, err := GetActionResult(api, c.requestedId, wait) 108 if err != nil { 109 return errors.Trace(err) 110 } 111 112 return c.out.Write(ctx, FormatActionResult(result)) 113 } 114 115 // GetActionResult tries to repeatedly fetch an action until it is 116 // in a completed state and then it returns it. 117 // It waits for a maximum of "wait" before returning with the latest action status. 118 func GetActionResult(api APIClient, requestedId string, wait *time.Timer) (params.ActionResult, error) { 119 120 // tick every two seconds, to delay the loop timer. 121 // TODO(fwereade): 2016-03-17 lp:1558657 122 tick := time.NewTimer(2 * time.Second) 123 124 return timerLoop(api, requestedId, wait, tick) 125 } 126 127 // timerLoop loops indefinitely to query the given API, until "wait" times 128 // out, using the "tick" timer to delay the API queries. It writes the 129 // result to the given output. 130 func timerLoop(api APIClient, requestedId string, wait, tick *time.Timer) (params.ActionResult, error) { 131 var ( 132 result params.ActionResult 133 err error 134 ) 135 136 // Loop over results until we get "failed" or "completed". Wait for 137 // timer, and reset it each time. 138 for { 139 result, err = fetchResult(api, requestedId) 140 if err != nil { 141 return result, err 142 } 143 144 // Whether or not we're waiting for a result, if a completed 145 // result arrives, we're done. 146 switch result.Status { 147 case params.ActionRunning, params.ActionPending: 148 default: 149 return result, nil 150 } 151 152 // Block until a tick happens, or the timeout arrives. 153 select { 154 case _ = <-wait.C: 155 return result, nil 156 157 case _ = <-tick.C: 158 tick.Reset(2 * time.Second) 159 } 160 } 161 } 162 163 // fetchResult queries the given API for the given Action ID prefix, and 164 // makes sure the results are acceptable, returning an error if they are not. 165 func fetchResult(api APIClient, requestedId string) (params.ActionResult, error) { 166 none := params.ActionResult{} 167 168 actionTag, err := getActionTagByPrefix(api, requestedId) 169 if err != nil { 170 return none, err 171 } 172 173 actions, err := api.Actions(params.Entities{ 174 Entities: []params.Entity{{actionTag.String()}}, 175 }) 176 if err != nil { 177 return none, err 178 } 179 actionResults := actions.Results 180 numActionResults := len(actionResults) 181 if numActionResults == 0 { 182 return none, errors.Errorf("no results for action %s", requestedId) 183 } 184 if numActionResults != 1 { 185 return none, errors.Errorf("too many results for action %s", requestedId) 186 } 187 188 result := actionResults[0] 189 if result.Error != nil { 190 return none, result.Error 191 } 192 193 return result, nil 194 } 195 196 // FormatActionResult removes empty values from the given ActionResult and 197 // inserts the remaining ones in a map[string]interface{} for cmd.Output to 198 // write in an easy-to-read format. 199 func FormatActionResult(result params.ActionResult) map[string]interface{} { 200 response := map[string]interface{}{"status": result.Status} 201 if result.Message != "" { 202 response["message"] = result.Message 203 } 204 if len(result.Output) != 0 { 205 response["results"] = result.Output 206 } 207 208 if result.Enqueued.IsZero() && result.Started.IsZero() && result.Completed.IsZero() { 209 return response 210 } 211 212 responseTiming := make(map[string]string) 213 for k, v := range map[string]time.Time{ 214 "enqueued": result.Enqueued, 215 "started": result.Started, 216 "completed": result.Completed, 217 } { 218 if !v.IsZero() { 219 responseTiming[k] = v.String() 220 } 221 } 222 response["timing"] = responseTiming 223 224 return response 225 }