github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/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  	"net"
    13  	"net/rpc"
    14  	"path/filepath"
    15  	"sort"
    16  	"sync"
    17  
    18  	"github.com/juju/cmd"
    19  	"github.com/juju/loggo"
    20  	"github.com/juju/utils/exec"
    21  	"github.com/juju/utils/featureflag"
    22  
    23  	"github.com/juju/juju/juju/sockets"
    24  	"github.com/juju/juju/storage"
    25  )
    26  
    27  var logger = loggo.GetLogger("worker.uniter.jujuc")
    28  
    29  // newCommands maps Command names to initializers.
    30  var newCommands = map[string]func(Context) cmd.Command{
    31  	"close-port" + cmdSuffix:    NewClosePortCommand,
    32  	"config-get" + cmdSuffix:    NewConfigGetCommand,
    33  	"juju-log" + cmdSuffix:      NewJujuLogCommand,
    34  	"open-port" + cmdSuffix:     NewOpenPortCommand,
    35  	"opened-ports" + cmdSuffix:  NewOpenedPortsCommand,
    36  	"relation-get" + cmdSuffix:  NewRelationGetCommand,
    37  	"action-get" + cmdSuffix:    NewActionGetCommand,
    38  	"action-set" + cmdSuffix:    NewActionSetCommand,
    39  	"action-fail" + cmdSuffix:   NewActionFailCommand,
    40  	"relation-ids" + cmdSuffix:  NewRelationIdsCommand,
    41  	"relation-list" + cmdSuffix: NewRelationListCommand,
    42  	"relation-set" + cmdSuffix:  NewRelationSetCommand,
    43  	"unit-get" + cmdSuffix:      NewUnitGetCommand,
    44  	"owner-get" + cmdSuffix:     NewOwnerGetCommand,
    45  	"add-metric" + cmdSuffix:    NewAddMetricCommand,
    46  	"juju-reboot" + cmdSuffix:   NewJujuRebootCommand,
    47  }
    48  
    49  var storageCommands = map[string]func(Context) cmd.Command{
    50  	"storage-get" + cmdSuffix: NewStorageGetCommand,
    51  }
    52  
    53  // CommandNames returns the names of all jujuc commands.
    54  func CommandNames() (names []string) {
    55  	for name := range newCommands {
    56  		names = append(names, name)
    57  	}
    58  	// TODO: stop checking feature flag once storage has graduated.
    59  	if featureflag.Enabled(storage.FeatureFlag) {
    60  		for name := range storageCommands {
    61  			names = append(names, name)
    62  		}
    63  	}
    64  	sort.Strings(names)
    65  	return
    66  }
    67  
    68  // NewCommand returns an instance of the named Command, initialized to execute
    69  // against the supplied Context.
    70  func NewCommand(ctx Context, name string) (cmd.Command, error) {
    71  	f := newCommands[name]
    72  	if f == nil && featureflag.Enabled(storage.FeatureFlag) {
    73  		f = storageCommands[name]
    74  	}
    75  	if f == nil {
    76  		return nil, fmt.Errorf("unknown command: %s", name)
    77  	}
    78  	return f(ctx), nil
    79  }
    80  
    81  // Request contains the information necessary to run a Command remotely.
    82  type Request struct {
    83  	ContextId   string
    84  	Dir         string
    85  	CommandName string
    86  	Args        []string
    87  }
    88  
    89  // CmdGetter looks up a Command implementation connected to a particular Context.
    90  type CmdGetter func(contextId, cmdName string) (cmd.Command, error)
    91  
    92  // Jujuc implements the jujuc command in the form required by net/rpc.
    93  type Jujuc struct {
    94  	mu     sync.Mutex
    95  	getCmd CmdGetter
    96  }
    97  
    98  // badReqErrorf returns an error indicating a bad Request.
    99  func badReqErrorf(format string, v ...interface{}) error {
   100  	return fmt.Errorf("bad request: "+format, v...)
   101  }
   102  
   103  // Main runs the Command specified by req, and fills in resp. A single command
   104  // is run at a time.
   105  func (j *Jujuc) Main(req Request, resp *exec.ExecResponse) error {
   106  	if req.CommandName == "" {
   107  		return badReqErrorf("command not specified")
   108  	}
   109  	if !filepath.IsAbs(req.Dir) {
   110  		return badReqErrorf("Dir is not absolute")
   111  	}
   112  	c, err := j.getCmd(req.ContextId, req.CommandName)
   113  	if err != nil {
   114  		return badReqErrorf("%s", err)
   115  	}
   116  	var stdin, stdout, stderr bytes.Buffer
   117  	ctx := &cmd.Context{
   118  		Dir:    req.Dir,
   119  		Stdin:  &stdin,
   120  		Stdout: &stdout,
   121  		Stderr: &stderr,
   122  	}
   123  	j.mu.Lock()
   124  	defer j.mu.Unlock()
   125  	logger.Infof("running hook tool %q %q", req.CommandName, req.Args)
   126  	logger.Debugf("hook context id %q; dir %q", req.ContextId, req.Dir)
   127  	resp.Code = cmd.Main(c, ctx, req.Args)
   128  	resp.Stdout = stdout.Bytes()
   129  	resp.Stderr = stderr.Bytes()
   130  	return nil
   131  }
   132  
   133  // Server implements a server that serves command invocations via
   134  // a unix domain socket.
   135  type Server struct {
   136  	socketPath string
   137  	listener   net.Listener
   138  	server     *rpc.Server
   139  	closed     chan bool
   140  	closing    chan bool
   141  	wg         sync.WaitGroup
   142  }
   143  
   144  // NewServer creates an RPC server bound to socketPath, which can execute
   145  // remote command invocations against an appropriate Context. It will not
   146  // actually do so until Run is called.
   147  func NewServer(getCmd CmdGetter, socketPath string) (*Server, error) {
   148  	server := rpc.NewServer()
   149  	if err := server.Register(&Jujuc{getCmd: getCmd}); err != nil {
   150  		return nil, err
   151  	}
   152  	listener, err := sockets.Listen(socketPath)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	s := &Server{
   157  		socketPath: socketPath,
   158  		listener:   listener,
   159  		server:     server,
   160  		closed:     make(chan bool),
   161  		closing:    make(chan bool),
   162  	}
   163  	return s, nil
   164  }
   165  
   166  // Run accepts new connections until it encounters an error, or until Close is
   167  // called, and then blocks until all existing connections have been closed.
   168  func (s *Server) Run() (err error) {
   169  	var conn net.Conn
   170  	for {
   171  		conn, err = s.listener.Accept()
   172  		if err != nil {
   173  			break
   174  		}
   175  		s.wg.Add(1)
   176  		go func(conn net.Conn) {
   177  			s.server.ServeConn(conn)
   178  			s.wg.Done()
   179  		}(conn)
   180  	}
   181  	select {
   182  	case <-s.closing:
   183  		// Someone has called Close(), so it is overwhelmingly likely that
   184  		// the error from Accept is a direct result of the Listener being
   185  		// closed, and can therefore be safely ignored.
   186  		err = nil
   187  	default:
   188  	}
   189  	s.wg.Wait()
   190  	close(s.closed)
   191  	return
   192  }
   193  
   194  // Close immediately stops accepting connections, and blocks until all existing
   195  // connections have been closed.
   196  func (s *Server) Close() {
   197  	close(s.closing)
   198  	s.listener.Close()
   199  	<-s.closed
   200  }