github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/jujud/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  	"fmt"
     8  	"os"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/juju/cmd"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/gnuflag"
    16  	"github.com/juju/mutex"
    17  	"github.com/juju/utils/clock"
    18  	"github.com/juju/utils/exec"
    19  	"gopkg.in/juju/names.v2"
    20  
    21  	"github.com/juju/juju/agent"
    22  	cmdutil "github.com/juju/juju/cmd/jujud/util"
    23  	"github.com/juju/juju/juju/sockets"
    24  	"github.com/juju/juju/worker/uniter"
    25  	jujuos "github.com/juju/utils/os"
    26  )
    27  
    28  type RunCommand struct {
    29  	cmd.CommandBase
    30  	MachineLockName string
    31  	unit            names.UnitTag
    32  	commands        string
    33  	showHelp        bool
    34  	noContext       bool
    35  	forceRemoteUnit bool
    36  	relationId      string
    37  	remoteUnitName  string
    38  }
    39  
    40  const runCommandDoc = `
    41  Run the specified commands in the hook context for the unit.
    42  
    43  unit-name can be either the unit tag:
    44   i.e.  unit-ubuntu-0
    45  or the unit id:
    46   i.e.  ubuntu/0
    47  
    48  If --no-context is specified, the <unit-name> positional
    49  argument is not needed.
    50  
    51  The commands are executed with '/bin/bash -s', and the output returned.
    52  `
    53  
    54  // Info returns usage information for the command.
    55  func (c *RunCommand) Info() *cmd.Info {
    56  	return &cmd.Info{
    57  		Name:    "juju-run",
    58  		Args:    "<unit-name> <commands>",
    59  		Purpose: "run commands in a unit's hook context",
    60  		Doc:     runCommandDoc,
    61  	}
    62  }
    63  
    64  func (c *RunCommand) SetFlags(f *gnuflag.FlagSet) {
    65  	f.BoolVar(&c.noContext, "no-context", false, "do not run the command in a unit context")
    66  	f.StringVar(&c.relationId, "r", "", "run the commands for a specific relation context on a unit")
    67  	f.StringVar(&c.relationId, "relation", "", "")
    68  	f.StringVar(&c.remoteUnitName, "remote-unit", "", "run the commands for a specific remote unit in a relation context on a unit")
    69  	f.BoolVar(&c.forceRemoteUnit, "force-remote-unit", false, "run the commands for a specific relation context, bypassing the remote unit check")
    70  }
    71  
    72  func (c *RunCommand) Init(args []string) error {
    73  	// make sure we aren't in an existing hook context
    74  	if contextId, err := getenv("JUJU_CONTEXT_ID"); err == nil && contextId != "" {
    75  		return fmt.Errorf("juju-run cannot be called from within a hook, have context %q", contextId)
    76  	}
    77  	if !c.noContext {
    78  		if len(args) < 1 {
    79  			return fmt.Errorf("missing unit-name")
    80  		}
    81  		var unitName string
    82  		unitName, args = args[0], args[1:]
    83  		// If the command line param is a unit id (like service/2) we need to
    84  		// change it to the unit tag as that is the format of the agent directory
    85  		// on disk (unit-application-2).
    86  		if names.IsValidUnit(unitName) {
    87  			c.unit = names.NewUnitTag(unitName)
    88  		} else {
    89  			var err error
    90  			c.unit, err = names.ParseUnitTag(unitName)
    91  			if err != nil {
    92  				return errors.Trace(err)
    93  			}
    94  		}
    95  	}
    96  	if len(args) < 1 {
    97  		return fmt.Errorf("missing commands")
    98  	}
    99  	c.commands, args = args[0], args[1:]
   100  	return cmd.CheckEmpty(args)
   101  }
   102  
   103  func (c *RunCommand) Run(ctx *cmd.Context) error {
   104  	var result *exec.ExecResponse
   105  	var err error
   106  	if c.noContext {
   107  		result, err = c.executeNoContext()
   108  	} else {
   109  		result, err = c.executeInUnitContext()
   110  	}
   111  	if err != nil {
   112  		return errors.Trace(err)
   113  	}
   114  
   115  	ctx.Stdout.Write(result.Stdout)
   116  	ctx.Stderr.Write(result.Stderr)
   117  	return cmd.NewRcPassthroughError(result.Code)
   118  }
   119  
   120  func (c *RunCommand) socketPath() string {
   121  	paths := uniter.NewPaths(cmdutil.DataDir, c.unit)
   122  	return paths.Runtime.JujuRunSocket
   123  }
   124  
   125  func (c *RunCommand) executeInUnitContext() (*exec.ExecResponse, error) {
   126  	unitDir := agent.Dir(cmdutil.DataDir, c.unit)
   127  	logger.Debugf("looking for unit dir %s", unitDir)
   128  	// make sure the unit exists
   129  	_, err := os.Stat(unitDir)
   130  	if os.IsNotExist(err) {
   131  		return nil, errors.Errorf("unit %q not found on this machine", c.unit.Id())
   132  	} else if err != nil {
   133  		return nil, errors.Trace(err)
   134  	}
   135  
   136  	relationId, err := checkRelationId(c.relationId)
   137  	if err != nil {
   138  		return nil, errors.Trace(err)
   139  	}
   140  
   141  	if len(c.remoteUnitName) > 0 && relationId == -1 {
   142  		return nil, errors.Errorf("remote unit: %s, provided without a relation", c.remoteUnitName)
   143  	}
   144  	client, err := sockets.Dial(c.socketPath())
   145  	if err != nil {
   146  		return nil, errors.Trace(err)
   147  	}
   148  	defer client.Close()
   149  
   150  	var result exec.ExecResponse
   151  	args := uniter.RunCommandsArgs{
   152  		Commands:        c.commands,
   153  		RelationId:      relationId,
   154  		RemoteUnitName:  c.remoteUnitName,
   155  		ForceRemoteUnit: c.forceRemoteUnit,
   156  	}
   157  	err = client.Call(uniter.JujuRunEndpoint, args, &result)
   158  	return &result, errors.Trace(err)
   159  }
   160  
   161  // appendProxyToCommands activates proxy settings on platforms
   162  // that support this feature via the command line. Currently this
   163  // will work on most GNU/Linux systems, but has no use in Windows
   164  // where the proxy settings are taken from the registry or from
   165  // application specific settings (proxy settings in firefox ignore
   166  // registry values on Windows).
   167  func (c *RunCommand) appendProxyToCommands() string {
   168  	switch jujuos.HostOS() {
   169  	case jujuos.Ubuntu:
   170  		return `[ -f "/home/ubuntu/.juju-proxy" ] && . "/home/ubuntu/.juju-proxy"` + "\n" + c.commands
   171  	default:
   172  		return c.commands
   173  	}
   174  }
   175  
   176  func (c *RunCommand) executeNoContext() (*exec.ExecResponse, error) {
   177  	// Acquire the uniter hook execution lock to make sure we don't
   178  	// stomp on each other.
   179  	spec := mutex.Spec{
   180  		Name:  c.MachineLockName,
   181  		Clock: clock.WallClock,
   182  		Delay: 250 * time.Millisecond,
   183  	}
   184  	logger.Debugf("acquire lock %q for juju-run", c.MachineLockName)
   185  	releaser, err := mutex.Acquire(spec)
   186  	if err != nil {
   187  		return nil, errors.Trace(err)
   188  	}
   189  	logger.Debugf("lock %q acquired", c.MachineLockName)
   190  
   191  	// Defer the logging first so it is executed after the Release. LIFO.
   192  	defer logger.Debugf("release lock %q for juju-run", c.MachineLockName)
   193  	defer releaser.Release()
   194  
   195  	runCmd := c.appendProxyToCommands()
   196  
   197  	return exec.RunCommands(
   198  		exec.RunParams{
   199  			Commands: runCmd,
   200  		})
   201  }
   202  
   203  // checkRelationId verifies that the relationId
   204  // given by the user is of a valid syntax, it does
   205  // not check that the relationId is a valid one. This
   206  // is done by the NewRunner method that is part of
   207  // the worker/uniter/runner/factory package.
   208  func checkRelationId(value string) (int, error) {
   209  	if len(value) == 0 {
   210  		return -1, nil
   211  	}
   212  
   213  	trim := value
   214  	if idx := strings.LastIndex(trim, ":"); idx != -1 {
   215  		trim = trim[idx+1:]
   216  	}
   217  	id, err := strconv.Atoi(trim)
   218  	if err != nil {
   219  		return -1, errors.Errorf("invalid relation id")
   220  	}
   221  	return id, nil
   222  }