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  }