github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/worker/uniter/runner/jujuc/server.go (about)

     1  // Copyright 2012, 2013, 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // The worker/uniter/runner/jujuc package implements the server side of the
     5  // jujuc proxy tool, which forwards command invocations to the unit agent
     6  // process so that they can be executed against specific state.
     7  package jujuc
     8  
     9  import (
    10  	"bytes"
    11  	"fmt"
    12  	"io"
    13  	"net"
    14  	"net/rpc"
    15  	"path/filepath"
    16  	"sort"
    17  	"sync"
    18  
    19  	"github.com/juju/cmd"
    20  	"github.com/juju/errors"
    21  	"github.com/juju/loggo"
    22  	"github.com/juju/utils/exec"
    23  
    24  	"github.com/juju/juju/juju/sockets"
    25  )
    26  
    27  // CmdSuffix is the filename suffix to use for executables.
    28  const CmdSuffix = cmdSuffix
    29  
    30  var logger = loggo.GetLogger("worker.uniter.jujuc")
    31  
    32  // ErrNoStdin is returned by Jujuc.Main if the hook tool requests
    33  // stdin, and none is supplied.
    34  var ErrNoStdin = errors.New("hook tool requires stdin, none supplied")
    35  
    36  type creator func(Context) (cmd.Command, error)
    37  
    38  var registeredCommands = map[string]creator{}
    39  
    40  func RegisterCommand(name string, f creator) {
    41  	registeredCommands[name+cmdSuffix] = f
    42  }
    43  
    44  // baseCommands maps Command names to creators.
    45  var baseCommands = map[string]creator{
    46  	"close-port" + cmdSuffix:    NewClosePortCommand,
    47  	"config-get" + cmdSuffix:    NewConfigGetCommand,
    48  	"juju-log" + cmdSuffix:      NewJujuLogCommand,
    49  	"open-port" + cmdSuffix:     NewOpenPortCommand,
    50  	"opened-ports" + cmdSuffix:  NewOpenedPortsCommand,
    51  	"relation-get" + cmdSuffix:  NewRelationGetCommand,
    52  	"action-get" + cmdSuffix:    NewActionGetCommand,
    53  	"action-set" + cmdSuffix:    NewActionSetCommand,
    54  	"action-fail" + cmdSuffix:   NewActionFailCommand,
    55  	"relation-ids" + cmdSuffix:  NewRelationIdsCommand,
    56  	"relation-list" + cmdSuffix: NewRelationListCommand,
    57  	"relation-set" + cmdSuffix:  NewRelationSetCommand,
    58  	"unit-get" + cmdSuffix:      NewUnitGetCommand,
    59  	"add-metric" + cmdSuffix:    NewAddMetricCommand,
    60  	"juju-reboot" + cmdSuffix:   NewJujuRebootCommand,
    61  	"status-get" + cmdSuffix:    NewStatusGetCommand,
    62  	"status-set" + cmdSuffix:    NewStatusSetCommand,
    63  	"network-get" + cmdSuffix:   NewNetworkGetCommand,
    64  }
    65  
    66  var storageCommands = map[string]creator{
    67  	"storage-add" + cmdSuffix:  NewStorageAddCommand,
    68  	"storage-get" + cmdSuffix:  NewStorageGetCommand,
    69  	"storage-list" + cmdSuffix: NewStorageListCommand,
    70  }
    71  
    72  var leaderCommands = map[string]creator{
    73  	"is-leader" + cmdSuffix:  NewIsLeaderCommand,
    74  	"leader-get" + cmdSuffix: NewLeaderGetCommand,
    75  	"leader-set" + cmdSuffix: NewLeaderSetCommand,
    76  }
    77  
    78  func allEnabledCommands() map[string]creator {
    79  	all := map[string]creator{}
    80  	add := func(m map[string]creator) {
    81  		for k, v := range m {
    82  			all[k] = v
    83  		}
    84  	}
    85  	add(baseCommands)
    86  	add(storageCommands)
    87  	add(leaderCommands)
    88  	add(registeredCommands)
    89  	return all
    90  }
    91  
    92  // CommandNames returns the names of all jujuc commands.
    93  func CommandNames() (names []string) {
    94  	for name := range allEnabledCommands() {
    95  		names = append(names, name)
    96  	}
    97  	sort.Strings(names)
    98  	return
    99  }
   100  
   101  // NewCommand returns an instance of the named Command, initialized to execute
   102  // against the supplied Context.
   103  func NewCommand(ctx Context, name string) (cmd.Command, error) {
   104  	f := allEnabledCommands()[name]
   105  	if f == nil {
   106  		return nil, errors.Errorf("unknown command: %s", name)
   107  	}
   108  	command, err := f(ctx)
   109  	if err != nil {
   110  		return nil, errors.Trace(err)
   111  	}
   112  	return command, nil
   113  }
   114  
   115  // Request contains the information necessary to run a Command remotely.
   116  type Request struct {
   117  	ContextId   string
   118  	Dir         string
   119  	CommandName string
   120  	Args        []string
   121  
   122  	// StdinSet indicates whether or not the client supplied stdin. This is
   123  	// necessary as Stdin will be nil if the client supplied stdin but it
   124  	// is empty.
   125  	StdinSet bool
   126  	Stdin    []byte
   127  }
   128  
   129  // CmdGetter looks up a Command implementation connected to a particular Context.
   130  type CmdGetter func(contextId, cmdName string) (cmd.Command, error)
   131  
   132  // Jujuc implements the jujuc command in the form required by net/rpc.
   133  type Jujuc struct {
   134  	mu     sync.Mutex
   135  	getCmd CmdGetter
   136  }
   137  
   138  // badReqErrorf returns an error indicating a bad Request.
   139  func badReqErrorf(format string, v ...interface{}) error {
   140  	return fmt.Errorf("bad request: "+format, v...)
   141  }
   142  
   143  // Main runs the Command specified by req, and fills in resp. A single command
   144  // is run at a time.
   145  func (j *Jujuc) Main(req Request, resp *exec.ExecResponse) error {
   146  	if req.CommandName == "" {
   147  		return badReqErrorf("command not specified")
   148  	}
   149  	if !filepath.IsAbs(req.Dir) {
   150  		return badReqErrorf("Dir is not absolute")
   151  	}
   152  	c, err := j.getCmd(req.ContextId, req.CommandName)
   153  	if err != nil {
   154  		return badReqErrorf("%s", err)
   155  	}
   156  	var stdin io.Reader
   157  	if req.StdinSet {
   158  		stdin = bytes.NewReader(req.Stdin)
   159  	} else {
   160  		// noStdinReader will error with ErrNoStdin
   161  		// if its Read method is called.
   162  		stdin = noStdinReader{}
   163  	}
   164  	var stdout, stderr bytes.Buffer
   165  	ctx := &cmd.Context{
   166  		Dir:    req.Dir,
   167  		Stdin:  stdin,
   168  		Stdout: &stdout,
   169  		Stderr: &stderr,
   170  	}
   171  	j.mu.Lock()
   172  	defer j.mu.Unlock()
   173  	logger.Infof("running hook tool %q %q", req.CommandName, req.Args)
   174  	logger.Debugf("hook context id %q; dir %q", req.ContextId, req.Dir)
   175  	wrapper := &cmdWrapper{c, nil}
   176  	resp.Code = cmd.Main(wrapper, ctx, req.Args)
   177  	if errors.Cause(wrapper.err) == ErrNoStdin {
   178  		return ErrNoStdin
   179  	}
   180  	resp.Stdout = stdout.Bytes()
   181  	resp.Stderr = stderr.Bytes()
   182  	return nil
   183  }
   184  
   185  // Server implements a server that serves command invocations via
   186  // a unix domain socket.
   187  type Server struct {
   188  	socketPath string
   189  	listener   net.Listener
   190  	server     *rpc.Server
   191  	closed     chan bool
   192  	closing    chan bool
   193  	wg         sync.WaitGroup
   194  }
   195  
   196  // NewServer creates an RPC server bound to socketPath, which can execute
   197  // remote command invocations against an appropriate Context. It will not
   198  // actually do so until Run is called.
   199  func NewServer(getCmd CmdGetter, socketPath string) (*Server, error) {
   200  	server := rpc.NewServer()
   201  	if err := server.Register(&Jujuc{getCmd: getCmd}); err != nil {
   202  		return nil, err
   203  	}
   204  	listener, err := sockets.Listen(socketPath)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  	s := &Server{
   209  		socketPath: socketPath,
   210  		listener:   listener,
   211  		server:     server,
   212  		closed:     make(chan bool),
   213  		closing:    make(chan bool),
   214  	}
   215  	return s, nil
   216  }
   217  
   218  // Run accepts new connections until it encounters an error, or until Close is
   219  // called, and then blocks until all existing connections have been closed.
   220  func (s *Server) Run() (err error) {
   221  	var conn net.Conn
   222  	for {
   223  		conn, err = s.listener.Accept()
   224  		if err != nil {
   225  			break
   226  		}
   227  		s.wg.Add(1)
   228  		go func(conn net.Conn) {
   229  			s.server.ServeConn(conn)
   230  			s.wg.Done()
   231  		}(conn)
   232  	}
   233  	select {
   234  	case <-s.closing:
   235  		// Someone has called Close(), so it is overwhelmingly likely that
   236  		// the error from Accept is a direct result of the Listener being
   237  		// closed, and can therefore be safely ignored.
   238  		err = nil
   239  	default:
   240  	}
   241  	s.wg.Wait()
   242  	close(s.closed)
   243  	return
   244  }
   245  
   246  // Close immediately stops accepting connections, and blocks until all existing
   247  // connections have been closed.
   248  func (s *Server) Close() {
   249  	close(s.closing)
   250  	s.listener.Close()
   251  	<-s.closed
   252  }
   253  
   254  type noStdinReader struct{}
   255  
   256  // Read implements io.Reader, simply returning ErrNoStdin any time it's called.
   257  func (noStdinReader) Read([]byte) (int, error) {
   258  	return 0, ErrNoStdin
   259  }
   260  
   261  // cmdWrapper wraps a cmd.Command's Run method so the error returned can be
   262  // intercepted when the command is run via cmd.Main.
   263  type cmdWrapper struct {
   264  	cmd.Command
   265  	err error
   266  }
   267  
   268  func (c *cmdWrapper) Run(ctx *cmd.Context) error {
   269  	c.err = c.Command.Run(ctx)
   270  	return c.err
   271  }