github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/cmd/juju/commands/run.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package commands
     5  
     6  import (
     7  	"encoding/base64"
     8  	"fmt"
     9  	"strings"
    10  	"time"
    11  	"unicode/utf8"
    12  
    13  	"github.com/juju/cmd"
    14  	"github.com/juju/names"
    15  	"launchpad.net/gnuflag"
    16  
    17  	"github.com/juju/juju/apiserver/params"
    18  	"github.com/juju/juju/cmd/envcmd"
    19  	"github.com/juju/juju/cmd/juju/block"
    20  )
    21  
    22  // RunCommand is responsible for running arbitrary commands on remote machines.
    23  type RunCommand struct {
    24  	envcmd.EnvCommandBase
    25  	out      cmd.Output
    26  	all      bool
    27  	timeout  time.Duration
    28  	machines []string
    29  	services []string
    30  	units    []string
    31  	commands string
    32  }
    33  
    34  const runDoc = `
    35  Run the commands on the specified targets.
    36  
    37  Targets are specified using either machine ids, service names or unit
    38  names.  At least one target specifier is needed.
    39  
    40  Multiple values can be set for --machine, --service, and --unit by using
    41  comma separated values.
    42  
    43  If the target is a machine, the command is run as the "ubuntu" user on
    44  the remote machine.
    45  
    46  If the target is a service, the command is run on all units for that
    47  service. For example, if there was a service "mysql" and that service
    48  had two units, "mysql/0" and "mysql/1", then
    49    --service mysql
    50  is equivalent to
    51    --unit mysql/0,mysql/1
    52  
    53  Commands run for services or units are executed in a 'hook context' for
    54  the unit.
    55  
    56  --all is provided as a simple way to run the command on all the machines
    57  in the environment.  If you specify --all you cannot provide additional
    58  targets.
    59  
    60  `
    61  
    62  func (c *RunCommand) Info() *cmd.Info {
    63  	return &cmd.Info{
    64  		Name:    "run",
    65  		Args:    "<commands>",
    66  		Purpose: "run the commands on the remote targets specified",
    67  		Doc:     runDoc,
    68  	}
    69  }
    70  
    71  func (c *RunCommand) SetFlags(f *gnuflag.FlagSet) {
    72  	c.out.AddFlags(f, "smart", cmd.DefaultFormatters)
    73  	f.BoolVar(&c.all, "all", false, "run the commands on all the machines")
    74  	f.DurationVar(&c.timeout, "timeout", 5*time.Minute, "how long to wait before the remote command is considered to have failed")
    75  	f.Var(cmd.NewStringsValue(nil, &c.machines), "machine", "one or more machine ids")
    76  	f.Var(cmd.NewStringsValue(nil, &c.services), "service", "one or more service names")
    77  	f.Var(cmd.NewStringsValue(nil, &c.units), "unit", "one or more unit ids")
    78  }
    79  
    80  func (c *RunCommand) Init(args []string) error {
    81  	if len(args) == 0 {
    82  		return fmt.Errorf("no commands specified")
    83  	}
    84  	c.commands, args = args[0], args[1:]
    85  
    86  	if c.all {
    87  		if len(c.machines) != 0 {
    88  			return fmt.Errorf("You cannot specify --all and individual machines")
    89  		}
    90  		if len(c.services) != 0 {
    91  			return fmt.Errorf("You cannot specify --all and individual services")
    92  		}
    93  		if len(c.units) != 0 {
    94  			return fmt.Errorf("You cannot specify --all and individual units")
    95  		}
    96  	} else {
    97  		if len(c.machines) == 0 && len(c.services) == 0 && len(c.units) == 0 {
    98  			return fmt.Errorf("You must specify a target, either through --all, --machine, --service or --unit")
    99  		}
   100  	}
   101  
   102  	var nameErrors []string
   103  	for _, machineId := range c.machines {
   104  		if !names.IsValidMachine(machineId) {
   105  			nameErrors = append(nameErrors, fmt.Sprintf("  %q is not a valid machine id", machineId))
   106  		}
   107  	}
   108  	for _, service := range c.services {
   109  		if !names.IsValidService(service) {
   110  			nameErrors = append(nameErrors, fmt.Sprintf("  %q is not a valid service name", service))
   111  		}
   112  	}
   113  	for _, unit := range c.units {
   114  		if !names.IsValidUnit(unit) {
   115  			nameErrors = append(nameErrors, fmt.Sprintf("  %q is not a valid unit name", unit))
   116  		}
   117  	}
   118  	if len(nameErrors) > 0 {
   119  		return fmt.Errorf("The following run targets are not valid:\n%s",
   120  			strings.Join(nameErrors, "\n"))
   121  	}
   122  
   123  	return cmd.CheckEmpty(args)
   124  }
   125  
   126  func encodeBytes(input []byte) (value string, encoding string) {
   127  	if utf8.Valid(input) {
   128  		value = string(input)
   129  		encoding = "utf8"
   130  	} else {
   131  		value = base64.StdEncoding.EncodeToString(input)
   132  		encoding = "base64"
   133  	}
   134  	return value, encoding
   135  }
   136  
   137  func storeOutput(values map[string]interface{}, key string, input []byte) {
   138  	value, encoding := encodeBytes(input)
   139  	values[key] = value
   140  	if encoding != "utf8" {
   141  		values[key+".encoding"] = encoding
   142  	}
   143  }
   144  
   145  // ConvertRunResults takes the results from the api and creates a map
   146  // suitable for format converstion to YAML or JSON.
   147  func ConvertRunResults(runResults []params.RunResult) interface{} {
   148  	var results = make([]interface{}, len(runResults))
   149  
   150  	for i, result := range runResults {
   151  		// We always want to have a string for stdout, but only show stderr,
   152  		// code and error if they are there.
   153  		values := make(map[string]interface{})
   154  		values["MachineId"] = result.MachineId
   155  		if result.UnitId != "" {
   156  			values["UnitId"] = result.UnitId
   157  
   158  		}
   159  		storeOutput(values, "Stdout", result.Stdout)
   160  		if len(result.Stderr) > 0 {
   161  			storeOutput(values, "Stderr", result.Stderr)
   162  		}
   163  		if result.Code != 0 {
   164  			values["ReturnCode"] = result.Code
   165  		}
   166  		if result.Error != "" {
   167  			values["Error"] = result.Error
   168  		}
   169  		results[i] = values
   170  	}
   171  
   172  	return results
   173  }
   174  
   175  func (c *RunCommand) Run(ctx *cmd.Context) error {
   176  	client, err := getRunAPIClient(c)
   177  	if err != nil {
   178  		return err
   179  	}
   180  	defer client.Close()
   181  
   182  	var runResults []params.RunResult
   183  	if c.all {
   184  		runResults, err = client.RunOnAllMachines(c.commands, c.timeout)
   185  	} else {
   186  		params := params.RunParams{
   187  			Commands: c.commands,
   188  			Timeout:  c.timeout,
   189  			Machines: c.machines,
   190  			Services: c.services,
   191  			Units:    c.units,
   192  		}
   193  		runResults, err = client.Run(params)
   194  	}
   195  
   196  	if err != nil {
   197  		return block.ProcessBlockedError(err, block.BlockChange)
   198  	}
   199  
   200  	// If we are just dealing with one result, AND we are using the smart
   201  	// format, then pretend we were running it locally.
   202  	if len(runResults) == 1 && c.out.Name() == "smart" {
   203  		result := runResults[0]
   204  		ctx.Stdout.Write(result.Stdout)
   205  		ctx.Stderr.Write(result.Stderr)
   206  		if result.Error != "" {
   207  			// Convert the error string back into an error object.
   208  			return fmt.Errorf("%s", result.Error)
   209  		}
   210  		if result.Code != 0 {
   211  			return cmd.NewRcPassthroughError(result.Code)
   212  		}
   213  		return nil
   214  	}
   215  
   216  	c.out.Write(ctx, ConvertRunResults(runResults))
   217  	return nil
   218  }
   219  
   220  // In order to be able to easily mock out the API side for testing,
   221  // the API client is got using a function.
   222  
   223  type RunClient interface {
   224  	Close() error
   225  	RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error)
   226  	Run(run params.RunParams) ([]params.RunResult, error)
   227  }
   228  
   229  // Here we need the signature to be correct for the interface.
   230  var getRunAPIClient = func(c *RunCommand) (RunClient, error) {
   231  	return c.NewAPIClient()
   232  }