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 }