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