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 }