github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/cmd/juju/run.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"encoding/base64"
     8  	"errors"
     9  	"fmt"
    10  	"strings"
    11  	"time"
    12  	"unicode/utf8"
    13  
    14  	"launchpad.net/gnuflag"
    15  
    16  	"launchpad.net/juju-core/cmd"
    17  	"launchpad.net/juju-core/juju"
    18  	"launchpad.net/juju-core/names"
    19  	"launchpad.net/juju-core/state/api/params"
    20  )
    21  
    22  // RunCommand is responsible for running arbitrary commands on remote machines.
    23  type RunCommand struct {
    24  	cmd.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.EnvCommandBase.SetFlags(f)
    73  	c.out.AddFlags(f, "smart", cmd.DefaultFormatters)
    74  	f.BoolVar(&c.all, "all", false, "run the commands on all the machines")
    75  	f.DurationVar(&c.timeout, "timeout", 5*time.Minute, "how long to wait before the remote command is considered to have failed")
    76  	f.Var(cmd.NewStringsValue(nil, &c.machines), "machine", "one or more machine ids")
    77  	f.Var(cmd.NewStringsValue(nil, &c.services), "service", "one or more service names")
    78  	f.Var(cmd.NewStringsValue(nil, &c.units), "unit", "one or more unit ids")
    79  }
    80  
    81  func (c *RunCommand) Init(args []string) error {
    82  	if len(args) == 0 {
    83  		return errors.New("no commands specified")
    84  	}
    85  	c.commands, args = args[0], args[1:]
    86  
    87  	if c.all {
    88  		if len(c.machines) != 0 {
    89  			return fmt.Errorf("You cannot specify --all and individual machines")
    90  		}
    91  		if len(c.services) != 0 {
    92  			return fmt.Errorf("You cannot specify --all and individual services")
    93  		}
    94  		if len(c.units) != 0 {
    95  			return fmt.Errorf("You cannot specify --all and individual units")
    96  		}
    97  	} else {
    98  		if len(c.machines) == 0 && len(c.services) == 0 && len(c.units) == 0 {
    99  			return fmt.Errorf("You must specify a target, either through --all, --machine, --service or --unit")
   100  		}
   101  	}
   102  
   103  	var nameErrors []string
   104  	for _, machineId := range c.machines {
   105  		if !names.IsMachine(machineId) {
   106  			nameErrors = append(nameErrors, fmt.Sprintf("  %q is not a valid machine id", machineId))
   107  		}
   108  	}
   109  	for _, service := range c.services {
   110  		if !names.IsService(service) {
   111  			nameErrors = append(nameErrors, fmt.Sprintf("  %q is not a valid service name", service))
   112  		}
   113  	}
   114  	for _, unit := range c.units {
   115  		if !names.IsUnit(unit) {
   116  			nameErrors = append(nameErrors, fmt.Sprintf("  %q is not a valid unit name", unit))
   117  		}
   118  	}
   119  	if len(nameErrors) > 0 {
   120  		return fmt.Errorf("The following run targets are not valid:\n%s",
   121  			strings.Join(nameErrors, "\n"))
   122  	}
   123  
   124  	return cmd.CheckEmpty(args)
   125  }
   126  
   127  func encodeBytes(input []byte) (value string, encoding string) {
   128  	if utf8.Valid(input) {
   129  		value = string(input)
   130  		encoding = "utf8"
   131  	} else {
   132  		value = base64.StdEncoding.EncodeToString(input)
   133  		encoding = "base64"
   134  	}
   135  	return value, encoding
   136  }
   137  
   138  func storeOutput(values map[string]interface{}, key string, input []byte) {
   139  	value, encoding := encodeBytes(input)
   140  	values[key] = value
   141  	if encoding != "utf8" {
   142  		values[key+".encoding"] = encoding
   143  	}
   144  }
   145  
   146  // ConvertRunResults takes the results from the api and creates a map
   147  // suitable for format converstion to YAML or JSON.
   148  func ConvertRunResults(runResults []params.RunResult) interface{} {
   149  	var results = make([]interface{}, len(runResults))
   150  
   151  	for i, result := range runResults {
   152  		// We always want to have a string for stdout, but only show stderr,
   153  		// code and error if they are there.
   154  		values := make(map[string]interface{})
   155  		values["MachineId"] = result.MachineId
   156  		if result.UnitId != "" {
   157  			values["UnitId"] = result.UnitId
   158  
   159  		}
   160  		storeOutput(values, "Stdout", result.Stdout)
   161  		if len(result.Stderr) > 0 {
   162  			storeOutput(values, "Stderr", result.Stderr)
   163  		}
   164  		if result.Code != 0 {
   165  			values["ReturnCode"] = result.Code
   166  		}
   167  		if result.Error != "" {
   168  			values["Error"] = result.Error
   169  		}
   170  		results[i] = values
   171  	}
   172  
   173  	return results
   174  }
   175  
   176  func (c *RunCommand) Run(ctx *cmd.Context) error {
   177  	client, err := getAPIClient(c.EnvName)
   178  	if err != nil {
   179  		return err
   180  	}
   181  	defer client.Close()
   182  
   183  	var runResults []params.RunResult
   184  	if c.all {
   185  		runResults, err = client.RunOnAllMachines(c.commands, c.timeout)
   186  	} else {
   187  		params := params.RunParams{
   188  			Commands: c.commands,
   189  			Timeout:  c.timeout,
   190  			Machines: c.machines,
   191  			Services: c.services,
   192  			Units:    c.units,
   193  		}
   194  		runResults, err = client.Run(params)
   195  	}
   196  
   197  	if err != nil {
   198  		return err
   199  	}
   200  
   201  	// If we are just dealing with one result, AND we are using the smart
   202  	// format, then pretend we were running it locally.
   203  	if len(runResults) == 1 && c.out.Name() == "smart" {
   204  		result := runResults[0]
   205  		ctx.Stdout.Write(result.Stdout)
   206  		ctx.Stderr.Write(result.Stderr)
   207  		if result.Error != "" {
   208  			// Convert the error string back into an error object.
   209  			return fmt.Errorf("%s", result.Error)
   210  		}
   211  		if result.Code != 0 {
   212  			return cmd.NewRcPassthroughError(result.Code)
   213  		}
   214  		return nil
   215  	}
   216  
   217  	c.out.Write(ctx, ConvertRunResults(runResults))
   218  	return nil
   219  }
   220  
   221  // In order to be able to easily mock out the API side for testing,
   222  // the API client is got using a function.
   223  
   224  type RunClient interface {
   225  	Close() error
   226  	RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error)
   227  	Run(run params.RunParams) ([]params.RunResult, error)
   228  }
   229  
   230  // Here we need the signature to be correct for the interface.
   231  var getAPIClient = func(name string) (RunClient, error) {
   232  	return juju.NewAPIClientFromName(name)
   233  }