github.com/zoomfoo/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/plugins/shared/cmd/launcher/command/device.go (about)

     1  package command
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"strings"
    11  
    12  	hclog "github.com/hashicorp/go-hclog"
    13  	plugin "github.com/hashicorp/go-plugin"
    14  	"github.com/hashicorp/hcl"
    15  	"github.com/hashicorp/hcl/hcl/ast"
    16  	hcl2 "github.com/hashicorp/hcl2/hcl"
    17  	"github.com/hashicorp/hcl2/hcldec"
    18  	"github.com/hashicorp/nomad/plugins/base"
    19  	"github.com/hashicorp/nomad/plugins/device"
    20  	"github.com/hashicorp/nomad/plugins/shared"
    21  	"github.com/hashicorp/nomad/plugins/shared/hclspec"
    22  	"github.com/kr/pretty"
    23  	"github.com/mitchellh/cli"
    24  	"github.com/zclconf/go-cty/cty/msgpack"
    25  )
    26  
    27  func DeviceCommandFactory(meta Meta) cli.CommandFactory {
    28  	return func() (cli.Command, error) {
    29  		return &Device{Meta: meta}, nil
    30  	}
    31  }
    32  
    33  type Device struct {
    34  	Meta
    35  
    36  	// dev is the plugin device
    37  	dev device.DevicePlugin
    38  
    39  	// spec is the returned and parsed spec.
    40  	spec hcldec.Spec
    41  }
    42  
    43  func (c *Device) Help() string {
    44  	helpText := `
    45  Usage: nomad-plugin-launcher device <device-binary> <config_file>
    46  
    47    Device launches the given device binary and provides a REPL for interacting
    48    with it.
    49  
    50  General Options:
    51  
    52  ` + generalOptionsUsage() + `
    53  
    54  Device Options:
    55    
    56    -trace
    57      Enable trace level log output.
    58  `
    59  
    60  	return strings.TrimSpace(helpText)
    61  }
    62  
    63  func (c *Device) Synopsis() string {
    64  	return "REPL for interacting with device plugins"
    65  }
    66  
    67  func (c *Device) Run(args []string) int {
    68  	var trace bool
    69  	cmdFlags := c.FlagSet("device")
    70  	cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
    71  	cmdFlags.BoolVar(&trace, "trace", false, "")
    72  
    73  	if err := cmdFlags.Parse(args); err != nil {
    74  		c.logger.Error("failed to parse flags:", "error", err)
    75  		return 1
    76  	}
    77  	if trace {
    78  		c.logger.SetLevel(hclog.Trace)
    79  	} else if c.verbose {
    80  		c.logger.SetLevel(hclog.Debug)
    81  	}
    82  
    83  	args = cmdFlags.Args()
    84  	numArgs := len(args)
    85  	if numArgs < 1 {
    86  		c.logger.Error("expected at least 1 args (device binary)", "args", args)
    87  		return 1
    88  	} else if numArgs > 2 {
    89  		c.logger.Error("expected at most 2 args (device binary and config file)", "args", args)
    90  		return 1
    91  	}
    92  
    93  	binary := args[0]
    94  	var config []byte
    95  	if numArgs == 2 {
    96  		var err error
    97  		config, err = ioutil.ReadFile(args[1])
    98  		if err != nil {
    99  			c.logger.Error("failed to read config file", "error", err)
   100  			return 1
   101  		}
   102  
   103  		c.logger.Trace("read config", "config", string(config))
   104  	}
   105  
   106  	// Get the plugin
   107  	dev, cleanup, err := c.getDevicePlugin(binary)
   108  	if err != nil {
   109  		c.logger.Error("failed to launch device plugin", "error", err)
   110  		return 1
   111  	}
   112  	defer cleanup()
   113  	c.dev = dev
   114  
   115  	spec, err := c.getSpec()
   116  	if err != nil {
   117  		c.logger.Error("failed to get config spec", "error", err)
   118  		return 1
   119  	}
   120  	c.spec = spec
   121  
   122  	if err := c.setConfig(spec, config); err != nil {
   123  		c.logger.Error("failed to set config", "error", err)
   124  		return 1
   125  	}
   126  
   127  	if err := c.startRepl(); err != nil {
   128  		c.logger.Error("error interacting with plugin", "error", err)
   129  		return 1
   130  	}
   131  
   132  	return 0
   133  }
   134  
   135  func (c *Device) getDevicePlugin(binary string) (device.DevicePlugin, func(), error) {
   136  	// Launch the plugin
   137  	client := plugin.NewClient(&plugin.ClientConfig{
   138  		HandshakeConfig: base.Handshake,
   139  		Plugins: map[string]plugin.Plugin{
   140  			base.PluginTypeBase:   &base.PluginBase{},
   141  			base.PluginTypeDevice: &device.PluginDevice{},
   142  		},
   143  		Cmd:              exec.Command(binary),
   144  		AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
   145  		Logger:           c.logger,
   146  	})
   147  
   148  	// Connect via RPC
   149  	rpcClient, err := client.Client()
   150  	if err != nil {
   151  		client.Kill()
   152  		return nil, nil, err
   153  	}
   154  
   155  	// Request the plugin
   156  	raw, err := rpcClient.Dispense(base.PluginTypeDevice)
   157  	if err != nil {
   158  		client.Kill()
   159  		return nil, nil, err
   160  	}
   161  
   162  	// We should have a KV store now! This feels like a normal interface
   163  	// implementation but is in fact over an RPC connection.
   164  	dev := raw.(device.DevicePlugin)
   165  	return dev, func() { client.Kill() }, nil
   166  }
   167  
   168  func (c *Device) getSpec() (hcldec.Spec, error) {
   169  	// Get the schema so we can parse the config
   170  	spec, err := c.dev.ConfigSchema()
   171  	if err != nil {
   172  		return nil, fmt.Errorf("failed to get config schema: %v", err)
   173  	}
   174  
   175  	c.logger.Trace("device spec", "spec", hclog.Fmt("% #v", pretty.Formatter(spec)))
   176  
   177  	// Convert the schema
   178  	schema, diag := hclspec.Convert(spec)
   179  	if diag.HasErrors() {
   180  		errStr := "failed to convert HCL schema: "
   181  		for _, err := range diag.Errs() {
   182  			errStr = fmt.Sprintf("%s\n* %s", errStr, err.Error())
   183  		}
   184  		return nil, errors.New(errStr)
   185  	}
   186  
   187  	return schema, nil
   188  }
   189  
   190  func (c *Device) setConfig(spec hcldec.Spec, config []byte) error {
   191  	// Parse the config into hcl
   192  	configVal, err := hclConfigToInterface(config)
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	c.logger.Trace("raw hcl config", "config", hclog.Fmt("% #v", pretty.Formatter(configVal)))
   198  
   199  	ctx := &hcl2.EvalContext{
   200  		Functions: shared.GetStdlibFuncs(),
   201  	}
   202  
   203  	val, diag := shared.ParseHclInterface(configVal, spec, ctx)
   204  	if diag.HasErrors() {
   205  		errStr := "failed to parse config"
   206  		for _, err := range diag.Errs() {
   207  			errStr = fmt.Sprintf("%s\n* %s", errStr, err.Error())
   208  		}
   209  		return errors.New(errStr)
   210  	}
   211  	c.logger.Trace("parsed hcl config", "config", hclog.Fmt("% #v", pretty.Formatter(val)))
   212  
   213  	cdata, err := msgpack.Marshal(val, val.Type())
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	c.logger.Trace("msgpack config", "config", string(cdata))
   219  	if err := c.dev.SetConfig(cdata); err != nil {
   220  		return err
   221  	}
   222  
   223  	return nil
   224  }
   225  
   226  func hclConfigToInterface(config []byte) (interface{}, error) {
   227  	if len(config) == 0 {
   228  		return map[string]interface{}{}, nil
   229  	}
   230  
   231  	// Parse as we do in the jobspec parser
   232  	root, err := hcl.Parse(string(config))
   233  	if err != nil {
   234  		return nil, fmt.Errorf("failed to hcl parse the config: %v", err)
   235  	}
   236  
   237  	// Top-level item should be a list
   238  	list, ok := root.Node.(*ast.ObjectList)
   239  	if !ok {
   240  		return nil, fmt.Errorf("root should be an object")
   241  	}
   242  
   243  	var m map[string]interface{}
   244  	if err := hcl.DecodeObject(&m, list.Items[0]); err != nil {
   245  		return nil, fmt.Errorf("failed to decode object: %v", err)
   246  	}
   247  
   248  	return m["config"], nil
   249  }
   250  
   251  func (c *Device) startRepl() error {
   252  	// Start the output goroutine
   253  	ctx, cancel := context.WithCancel(context.Background())
   254  	defer cancel()
   255  	fingerprint := make(chan context.Context)
   256  	stats := make(chan context.Context)
   257  	reserve := make(chan []string)
   258  	go c.replOutput(ctx, fingerprint, stats, reserve)
   259  
   260  	c.Ui.Output("> Availabile commands are: exit(), fingerprint(), stop_fingerprint(), stats(), stop_stats(), reserve(id1, id2, ...)")
   261  	var fingerprintCtx, statsCtx context.Context
   262  	var fingerprintCancel, statsCancel context.CancelFunc
   263  
   264  	for {
   265  		in, err := c.Ui.Ask("> ")
   266  		if err != nil {
   267  			if fingerprintCancel != nil {
   268  				fingerprintCancel()
   269  			}
   270  			if statsCancel != nil {
   271  				statsCancel()
   272  			}
   273  			return err
   274  		}
   275  
   276  		switch {
   277  		case in == "exit()":
   278  			if fingerprintCancel != nil {
   279  				fingerprintCancel()
   280  			}
   281  			if statsCancel != nil {
   282  				statsCancel()
   283  			}
   284  			return nil
   285  		case in == "fingerprint()":
   286  			if fingerprintCtx != nil {
   287  				continue
   288  			}
   289  			fingerprintCtx, fingerprintCancel = context.WithCancel(ctx)
   290  			fingerprint <- fingerprintCtx
   291  		case in == "stop_fingerprint()":
   292  			if fingerprintCtx == nil {
   293  				continue
   294  			}
   295  			fingerprintCancel()
   296  			fingerprintCtx = nil
   297  		case in == "stats()":
   298  			if statsCtx != nil {
   299  				continue
   300  			}
   301  			statsCtx, statsCancel = context.WithCancel(ctx)
   302  			stats <- statsCtx
   303  		case in == "stop_stats()":
   304  			if statsCtx == nil {
   305  				continue
   306  			}
   307  			statsCancel()
   308  			statsCtx = nil
   309  		case strings.HasPrefix(in, "reserve(") && strings.HasSuffix(in, ")"):
   310  			listString := strings.TrimSuffix(strings.TrimPrefix(in, "reserve("), ")")
   311  			ids := strings.Split(strings.TrimSpace(listString), ",")
   312  			reserve <- ids
   313  		default:
   314  			c.Ui.Error(fmt.Sprintf("> Unknown command %q", in))
   315  		}
   316  	}
   317  }
   318  
   319  func (c *Device) replOutput(ctx context.Context, startFingerprint, startStats <-chan context.Context, reserve <-chan []string) {
   320  	var fingerprint <-chan *device.FingerprintResponse
   321  	var stats <-chan *device.StatsResponse
   322  	for {
   323  		select {
   324  		case <-ctx.Done():
   325  			return
   326  		case ctx := <-startFingerprint:
   327  			var err error
   328  			fingerprint, err = c.dev.Fingerprint(ctx)
   329  			if err != nil {
   330  				c.Ui.Error(fmt.Sprintf("fingerprint: %s", err))
   331  				os.Exit(1)
   332  			}
   333  		case resp, ok := <-fingerprint:
   334  			if !ok {
   335  				c.Ui.Output("> fingerprint: fingerprint output closed")
   336  				fingerprint = nil
   337  				continue
   338  			}
   339  
   340  			if resp == nil {
   341  				c.Ui.Warn("> fingerprint: received nil result")
   342  				os.Exit(1)
   343  			}
   344  
   345  			c.Ui.Output(fmt.Sprintf("> fingerprint: % #v", pretty.Formatter(resp)))
   346  		case ctx := <-startStats:
   347  			var err error
   348  			stats, err = c.dev.Stats(ctx)
   349  			if err != nil {
   350  				c.Ui.Error(fmt.Sprintf("stats: %s", err))
   351  				os.Exit(1)
   352  			}
   353  		case resp, ok := <-stats:
   354  			if !ok {
   355  				c.Ui.Output("> stats: stats output closed")
   356  				stats = nil
   357  				continue
   358  			}
   359  
   360  			if resp == nil {
   361  				c.Ui.Warn("> stats: received nil result")
   362  				os.Exit(1)
   363  			}
   364  
   365  			c.Ui.Output(fmt.Sprintf("> stats: % #v", pretty.Formatter(resp)))
   366  		case ids := <-reserve:
   367  			resp, err := c.dev.Reserve(ids)
   368  			if err != nil {
   369  				c.Ui.Warn(fmt.Sprintf("> reserve(%s): %v", strings.Join(ids, ", "), err))
   370  			} else {
   371  				c.Ui.Output(fmt.Sprintf("> reserve(%s): % #v", strings.Join(ids, ", "), pretty.Formatter(resp)))
   372  			}
   373  		}
   374  	}
   375  }