github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/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  
   105  	// Get the plugin
   106  	dev, cleanup, err := c.getDevicePlugin(binary)
   107  	if err != nil {
   108  		c.logger.Error("failed to launch device plugin", "error", err)
   109  		return 1
   110  	}
   111  	defer cleanup()
   112  	c.dev = dev
   113  
   114  	spec, err := c.getSpec()
   115  	if err != nil {
   116  		c.logger.Error("failed to get config spec", "error", err)
   117  		return 1
   118  	}
   119  	c.spec = spec
   120  
   121  	if err := c.setConfig(spec, device.ApiVersion010, config, nil); err != nil {
   122  		c.logger.Error("failed to set config", "error", err)
   123  		return 1
   124  	}
   125  
   126  	if err := c.startRepl(); err != nil {
   127  		c.logger.Error("error interacting with plugin", "error", err)
   128  		return 1
   129  	}
   130  
   131  	return 0
   132  }
   133  
   134  func (c *Device) getDevicePlugin(binary string) (device.DevicePlugin, func(), error) {
   135  	// Launch the plugin
   136  	client := plugin.NewClient(&plugin.ClientConfig{
   137  		HandshakeConfig: base.Handshake,
   138  		Plugins: map[string]plugin.Plugin{
   139  			base.PluginTypeBase:   &base.PluginBase{},
   140  			base.PluginTypeDevice: &device.PluginDevice{},
   141  		},
   142  		Cmd:              exec.Command(binary),
   143  		AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
   144  		Logger:           c.logger,
   145  	})
   146  
   147  	// Connect via RPC
   148  	rpcClient, err := client.Client()
   149  	if err != nil {
   150  		client.Kill()
   151  		return nil, nil, err
   152  	}
   153  
   154  	// Request the plugin
   155  	raw, err := rpcClient.Dispense(base.PluginTypeDevice)
   156  	if err != nil {
   157  		client.Kill()
   158  		return nil, nil, err
   159  	}
   160  
   161  	// We should have a KV store now! This feels like a normal interface
   162  	// implementation but is in fact over an RPC connection.
   163  	dev := raw.(device.DevicePlugin)
   164  	return dev, func() { client.Kill() }, nil
   165  }
   166  
   167  func (c *Device) getSpec() (hcldec.Spec, error) {
   168  	// Get the schema so we can parse the config
   169  	spec, err := c.dev.ConfigSchema()
   170  	if err != nil {
   171  		return nil, fmt.Errorf("failed to get config schema: %v", err)
   172  	}
   173  
   174  	// Convert the schema
   175  	schema, diag := hclspecutils.Convert(spec)
   176  	if diag.HasErrors() {
   177  		errStr := "failed to convert HCL schema: "
   178  		for _, err := range diag.Errs() {
   179  			errStr = fmt.Sprintf("%s\n* %s", errStr, err.Error())
   180  		}
   181  		return nil, errors.New(errStr)
   182  	}
   183  
   184  	return schema, nil
   185  }
   186  
   187  func (c *Device) setConfig(spec hcldec.Spec, apiVersion string, config []byte, nmdCfg *base.AgentConfig) error {
   188  	// Parse the config into hcl
   189  	configVal, err := hclConfigToInterface(config)
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	val, diag, diagErrs := hclutils.ParseHclInterface(configVal, spec, nil)
   195  	if diag.HasErrors() {
   196  		return multierror.Append(errors.New("failed to parse config: "), diagErrs...)
   197  	}
   198  
   199  	cdata, err := msgpack.Marshal(val, val.Type())
   200  	if err != nil {
   201  		return err
   202  	}
   203  
   204  	req := &base.Config{
   205  		PluginConfig: cdata,
   206  		AgentConfig:  nmdCfg,
   207  		ApiVersion:   apiVersion,
   208  	}
   209  
   210  	if err := c.dev.SetConfig(req); err != nil {
   211  		return err
   212  	}
   213  
   214  	return nil
   215  }
   216  
   217  func hclConfigToInterface(config []byte) (interface{}, error) {
   218  	if len(config) == 0 {
   219  		return map[string]interface{}{}, nil
   220  	}
   221  
   222  	// Parse as we do in the jobspec parser
   223  	root, err := hcl.Parse(string(config))
   224  	if err != nil {
   225  		return nil, fmt.Errorf("failed to hcl parse the config: %v", err)
   226  	}
   227  
   228  	// Top-level item should be a list
   229  	list, ok := root.Node.(*ast.ObjectList)
   230  	if !ok {
   231  		return nil, fmt.Errorf("root should be an object")
   232  	}
   233  
   234  	var m map[string]interface{}
   235  	if err := hcl.DecodeObject(&m, list.Items[0]); err != nil {
   236  		return nil, fmt.Errorf("failed to decode object: %v", err)
   237  	}
   238  
   239  	return m["config"], nil
   240  }
   241  
   242  func (c *Device) startRepl() error {
   243  	// Start the output goroutine
   244  	ctx, cancel := context.WithCancel(context.Background())
   245  	defer cancel()
   246  	fingerprint := make(chan context.Context)
   247  	stats := make(chan context.Context)
   248  	reserve := make(chan []string)
   249  	go c.replOutput(ctx, fingerprint, stats, reserve)
   250  
   251  	c.Ui.Output("> Availabile commands are: exit(), fingerprint(), stop_fingerprint(), stats(), stop_stats(), reserve(id1, id2, ...)")
   252  	var fingerprintCtx, statsCtx context.Context
   253  	var fingerprintCancel, statsCancel context.CancelFunc
   254  
   255  	for {
   256  		in, err := c.Ui.Ask("> ")
   257  		if err != nil {
   258  			if fingerprintCancel != nil {
   259  				fingerprintCancel()
   260  			}
   261  			if statsCancel != nil {
   262  				statsCancel()
   263  			}
   264  			return err
   265  		}
   266  
   267  		switch {
   268  		case in == "exit()":
   269  			if fingerprintCancel != nil {
   270  				fingerprintCancel()
   271  			}
   272  			if statsCancel != nil {
   273  				statsCancel()
   274  			}
   275  			return nil
   276  		case in == "fingerprint()":
   277  			if fingerprintCtx != nil {
   278  				continue
   279  			}
   280  			fingerprintCtx, fingerprintCancel = context.WithCancel(ctx)
   281  			fingerprint <- fingerprintCtx
   282  		case in == "stop_fingerprint()":
   283  			if fingerprintCtx == nil {
   284  				continue
   285  			}
   286  			fingerprintCancel()
   287  			fingerprintCtx = nil
   288  		case in == "stats()":
   289  			if statsCtx != nil {
   290  				continue
   291  			}
   292  			statsCtx, statsCancel = context.WithCancel(ctx)
   293  			stats <- statsCtx
   294  		case in == "stop_stats()":
   295  			if statsCtx == nil {
   296  				continue
   297  			}
   298  			statsCancel()
   299  			statsCtx = nil
   300  		case strings.HasPrefix(in, "reserve(") && strings.HasSuffix(in, ")"):
   301  			listString := strings.TrimSuffix(strings.TrimPrefix(in, "reserve("), ")")
   302  			ids := strings.Split(strings.TrimSpace(listString), ",")
   303  			reserve <- ids
   304  		default:
   305  			c.Ui.Error(fmt.Sprintf("> Unknown command %q", in))
   306  		}
   307  	}
   308  }
   309  
   310  func (c *Device) replOutput(ctx context.Context, startFingerprint, startStats <-chan context.Context, reserve <-chan []string) {
   311  	var fingerprint <-chan *device.FingerprintResponse
   312  	var stats <-chan *device.StatsResponse
   313  	for {
   314  		select {
   315  		case <-ctx.Done():
   316  			return
   317  		case ctx := <-startFingerprint:
   318  			var err error
   319  			fingerprint, err = c.dev.Fingerprint(ctx)
   320  			if err != nil {
   321  				c.Ui.Error(fmt.Sprintf("fingerprint: %s", err))
   322  				os.Exit(1)
   323  			}
   324  		case resp, ok := <-fingerprint:
   325  			if !ok {
   326  				c.Ui.Output("> fingerprint: fingerprint output closed")
   327  				fingerprint = nil
   328  				continue
   329  			}
   330  
   331  			if resp == nil {
   332  				c.Ui.Warn("> fingerprint: received nil result")
   333  				os.Exit(1)
   334  			}
   335  
   336  			c.Ui.Output(fmt.Sprintf("> fingerprint: % #v", pretty.Formatter(resp)))
   337  		case ctx := <-startStats:
   338  			var err error
   339  			stats, err = c.dev.Stats(ctx, 1*time.Second)
   340  			if err != nil {
   341  				c.Ui.Error(fmt.Sprintf("stats: %s", err))
   342  				os.Exit(1)
   343  			}
   344  		case resp, ok := <-stats:
   345  			if !ok {
   346  				c.Ui.Output("> stats: stats output closed")
   347  				stats = nil
   348  				continue
   349  			}
   350  
   351  			if resp == nil {
   352  				c.Ui.Warn("> stats: received nil result")
   353  				os.Exit(1)
   354  			}
   355  
   356  			c.Ui.Output(fmt.Sprintf("> stats: % #v", pretty.Formatter(resp)))
   357  		case ids := <-reserve:
   358  			resp, err := c.dev.Reserve(ids)
   359  			if err != nil {
   360  				c.Ui.Warn(fmt.Sprintf("> reserve(%s): %v", strings.Join(ids, ", "), err))
   361  			} else {
   362  				c.Ui.Output(fmt.Sprintf("> reserve(%s): % #v", strings.Join(ids, ", "), pretty.Formatter(resp)))
   363  			}
   364  		}
   365  	}
   366  }