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