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  }