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 }