github.com/bigcommerce/nomad@v0.9.3-bc/drivers/qemu/driver.go (about) 1 package qemu 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net" 8 "os/exec" 9 "path/filepath" 10 "regexp" 11 "runtime" 12 "strings" 13 "time" 14 15 "github.com/coreos/go-semver/semver" 16 hclog "github.com/hashicorp/go-hclog" 17 "github.com/hashicorp/nomad/drivers/shared/eventer" 18 "github.com/hashicorp/nomad/drivers/shared/executor" 19 "github.com/hashicorp/nomad/helper/pluginutils/hclutils" 20 "github.com/hashicorp/nomad/helper/pluginutils/loader" 21 "github.com/hashicorp/nomad/plugins/base" 22 "github.com/hashicorp/nomad/plugins/drivers" 23 "github.com/hashicorp/nomad/plugins/shared/hclspec" 24 pstructs "github.com/hashicorp/nomad/plugins/shared/structs" 25 ) 26 27 const ( 28 // pluginName is the name of the plugin 29 pluginName = "qemu" 30 31 // fingerprintPeriod is the interval at which the driver will send fingerprint responses 32 fingerprintPeriod = 30 * time.Second 33 34 // The key populated in Node Attributes to indicate presence of the Qemu driver 35 driverAttr = "driver.qemu" 36 driverVersionAttr = "driver.qemu.version" 37 38 // Represents an ACPI shutdown request to the VM (emulates pressing a physical power button) 39 // Reference: https://en.wikibooks.org/wiki/QEMU/Monitor 40 qemuGracefulShutdownMsg = "system_powerdown\n" 41 qemuMonitorSocketName = "qemu-monitor.sock" 42 43 // Maximum socket path length prior to qemu 2.10.1 44 qemuLegacyMaxMonitorPathLen = 108 45 46 // taskHandleVersion is the version of task handle which this driver sets 47 // and understands how to decode driver state 48 taskHandleVersion = 1 49 ) 50 51 var ( 52 // PluginID is the qemu plugin metadata registered in the plugin 53 // catalog. 54 PluginID = loader.PluginID{ 55 Name: pluginName, 56 PluginType: base.PluginTypeDriver, 57 } 58 59 // PluginConfig is the qemu driver factory function registered in the 60 // plugin catalog. 61 PluginConfig = &loader.InternalPluginConfig{ 62 Config: map[string]interface{}{}, 63 Factory: func(l hclog.Logger) interface{} { return NewQemuDriver(l) }, 64 } 65 66 versionRegex = regexp.MustCompile(`version (\d[\.\d+]+)`) 67 68 // Prior to qemu 2.10.1, monitor socket paths are truncated to 108 bytes. 69 // We should consider this if driver.qemu.version is < 2.10.1 and the 70 // generated monitor path is too long. 71 // 72 // Relevant fix is here: 73 // https://github.com/qemu/qemu/commit/ad9579aaa16d5b385922d49edac2c96c79bcfb6 74 qemuVersionLongSocketPathFix = semver.New("2.10.1") 75 76 // pluginInfo is the response returned for the PluginInfo RPC 77 pluginInfo = &base.PluginInfoResponse{ 78 Type: base.PluginTypeDriver, 79 PluginApiVersions: []string{drivers.ApiVersion010}, 80 PluginVersion: "0.1.0", 81 Name: pluginName, 82 } 83 84 // configSpec is the hcl specification returned by the ConfigSchema RPC 85 configSpec = hclspec.NewObject(map[string]*hclspec.Spec{}) 86 87 // taskConfigSpec is the hcl specification for the driver config section of 88 // a taskConfig within a job. It is returned in the TaskConfigSchema RPC 89 taskConfigSpec = hclspec.NewObject(map[string]*hclspec.Spec{ 90 "image_path": hclspec.NewAttr("image_path", "string", true), 91 "accelerator": hclspec.NewAttr("accelerator", "string", false), 92 "graceful_shutdown": hclspec.NewAttr("graceful_shutdown", "bool", false), 93 "args": hclspec.NewAttr("args", "list(string)", false), 94 "port_map": hclspec.NewAttr("port_map", "list(map(number))", false), 95 }) 96 97 // capabilities is returned by the Capabilities RPC and indicates what 98 // optional features this driver supports 99 capabilities = &drivers.Capabilities{ 100 SendSignals: false, 101 Exec: false, 102 FSIsolation: drivers.FSIsolationImage, 103 } 104 105 _ drivers.DriverPlugin = (*Driver)(nil) 106 ) 107 108 // TaskConfig is the driver configuration of a taskConfig within a job 109 type TaskConfig struct { 110 ImagePath string `codec:"image_path"` 111 Accelerator string `codec:"accelerator"` 112 Args []string `codec:"args"` // extra arguments to qemu executable 113 PortMap hclutils.MapStrInt `codec:"port_map"` // A map of host port and the port name defined in the image manifest file 114 GracefulShutdown bool `codec:"graceful_shutdown"` 115 } 116 117 // TaskState is the state which is encoded in the handle returned in StartTask. 118 // This information is needed to rebuild the taskConfig state and handler 119 // during recovery. 120 type TaskState struct { 121 ReattachConfig *pstructs.ReattachConfig 122 TaskConfig *drivers.TaskConfig 123 Pid int 124 StartedAt time.Time 125 } 126 127 // Driver is a driver for running images via Qemu 128 type Driver struct { 129 // eventer is used to handle multiplexing of TaskEvents calls such that an 130 // event can be broadcast to all callers 131 eventer *eventer.Eventer 132 133 // tasks is the in memory datastore mapping taskIDs to qemuTaskHandle 134 tasks *taskStore 135 136 // ctx is the context for the driver. It is passed to other subsystems to 137 // coordinate shutdown 138 ctx context.Context 139 140 // nomadConf is the client agent's configuration 141 nomadConfig *base.ClientDriverConfig 142 143 // signalShutdown is called when the driver is shutting down and cancels the 144 // ctx passed to any subsystems 145 signalShutdown context.CancelFunc 146 147 // logger will log to the Nomad agent 148 logger hclog.Logger 149 } 150 151 func NewQemuDriver(logger hclog.Logger) drivers.DriverPlugin { 152 ctx, cancel := context.WithCancel(context.Background()) 153 logger = logger.Named(pluginName) 154 return &Driver{ 155 eventer: eventer.NewEventer(ctx, logger), 156 tasks: newTaskStore(), 157 ctx: ctx, 158 signalShutdown: cancel, 159 logger: logger, 160 } 161 } 162 163 func (d *Driver) PluginInfo() (*base.PluginInfoResponse, error) { 164 return pluginInfo, nil 165 } 166 167 func (d *Driver) ConfigSchema() (*hclspec.Spec, error) { 168 return configSpec, nil 169 } 170 171 func (d *Driver) SetConfig(cfg *base.Config) error { 172 if cfg.AgentConfig != nil { 173 d.nomadConfig = cfg.AgentConfig.Driver 174 } 175 return nil 176 } 177 178 func (d *Driver) TaskConfigSchema() (*hclspec.Spec, error) { 179 return taskConfigSpec, nil 180 } 181 182 func (d *Driver) Capabilities() (*drivers.Capabilities, error) { 183 return capabilities, nil 184 } 185 186 func (d *Driver) Fingerprint(ctx context.Context) (<-chan *drivers.Fingerprint, error) { 187 ch := make(chan *drivers.Fingerprint) 188 go d.handleFingerprint(ctx, ch) 189 return ch, nil 190 } 191 192 func (d *Driver) handleFingerprint(ctx context.Context, ch chan *drivers.Fingerprint) { 193 ticker := time.NewTimer(0) 194 for { 195 select { 196 case <-ctx.Done(): 197 return 198 case <-d.ctx.Done(): 199 return 200 case <-ticker.C: 201 ticker.Reset(fingerprintPeriod) 202 ch <- d.buildFingerprint() 203 } 204 } 205 } 206 207 func (d *Driver) buildFingerprint() *drivers.Fingerprint { 208 fingerprint := &drivers.Fingerprint{ 209 Attributes: map[string]*pstructs.Attribute{}, 210 Health: drivers.HealthStateHealthy, 211 HealthDescription: drivers.DriverHealthy, 212 } 213 214 bin := "qemu-system-x86_64" 215 if runtime.GOOS == "windows" { 216 // On windows, the "qemu-system-x86_64" command does not respond to the 217 // version flag. 218 bin = "qemu-img" 219 } 220 outBytes, err := exec.Command(bin, "--version").Output() 221 if err != nil { 222 // return no error, as it isn't an error to not find qemu, it just means we 223 // can't use it. 224 fingerprint.Health = drivers.HealthStateUndetected 225 fingerprint.HealthDescription = "" 226 return fingerprint 227 } 228 out := strings.TrimSpace(string(outBytes)) 229 230 matches := versionRegex.FindStringSubmatch(out) 231 if len(matches) != 2 { 232 fingerprint.Health = drivers.HealthStateUndetected 233 fingerprint.HealthDescription = fmt.Sprintf("Failed to parse qemu version from %v", out) 234 return fingerprint 235 } 236 currentQemuVersion := matches[1] 237 fingerprint.Attributes[driverAttr] = pstructs.NewBoolAttribute(true) 238 fingerprint.Attributes[driverVersionAttr] = pstructs.NewStringAttribute(currentQemuVersion) 239 return fingerprint 240 } 241 242 func (d *Driver) RecoverTask(handle *drivers.TaskHandle) error { 243 if handle == nil { 244 return fmt.Errorf("error: handle cannot be nil") 245 } 246 247 // COMPAT(0.10): pre 0.9 upgrade path check 248 if handle.Version == 0 { 249 return d.recoverPre09Task(handle) 250 } 251 252 // If already attached to handle there's nothing to recover. 253 if _, ok := d.tasks.Get(handle.Config.ID); ok { 254 d.logger.Trace("nothing to recover; task already exists", 255 "task_id", handle.Config.ID, 256 "task_name", handle.Config.Name, 257 ) 258 return nil 259 } 260 261 var taskState TaskState 262 if err := handle.GetDriverState(&taskState); err != nil { 263 d.logger.Error("failed to decode taskConfig state from handle", "error", err, "task_id", handle.Config.ID) 264 return fmt.Errorf("failed to decode taskConfig state from handle: %v", err) 265 } 266 267 plugRC, err := pstructs.ReattachConfigToGoPlugin(taskState.ReattachConfig) 268 if err != nil { 269 d.logger.Error("failed to build ReattachConfig from taskConfig state", "error", err, "task_id", handle.Config.ID) 270 return fmt.Errorf("failed to build ReattachConfig from taskConfig state: %v", err) 271 } 272 273 execImpl, pluginClient, err := executor.ReattachToExecutor(plugRC, 274 d.logger.With("task_name", handle.Config.Name, "alloc_id", handle.Config.AllocID)) 275 if err != nil { 276 d.logger.Error("failed to reattach to executor", "error", err, "task_id", handle.Config.ID) 277 return fmt.Errorf("failed to reattach to executor: %v", err) 278 } 279 280 h := &taskHandle{ 281 exec: execImpl, 282 pid: taskState.Pid, 283 pluginClient: pluginClient, 284 taskConfig: taskState.TaskConfig, 285 procState: drivers.TaskStateRunning, 286 startedAt: taskState.StartedAt, 287 exitResult: &drivers.ExitResult{}, 288 } 289 290 d.tasks.Set(taskState.TaskConfig.ID, h) 291 292 go h.run() 293 return nil 294 } 295 296 func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drivers.DriverNetwork, error) { 297 if _, ok := d.tasks.Get(cfg.ID); ok { 298 return nil, nil, fmt.Errorf("taskConfig with ID '%s' already started", cfg.ID) 299 } 300 301 var driverConfig TaskConfig 302 303 if err := cfg.DecodeDriverConfig(&driverConfig); err != nil { 304 return nil, nil, fmt.Errorf("failed to decode driver config: %v", err) 305 } 306 307 handle := drivers.NewTaskHandle(taskHandleVersion) 308 handle.Config = cfg 309 310 // Get the image source 311 vmPath := driverConfig.ImagePath 312 if vmPath == "" { 313 return nil, nil, fmt.Errorf("image_path must be set") 314 } 315 vmID := filepath.Base(vmPath) 316 317 // Parse configuration arguments 318 // Create the base arguments 319 accelerator := "tcg" 320 if driverConfig.Accelerator != "" { 321 accelerator = driverConfig.Accelerator 322 } 323 324 mb := cfg.Resources.NomadResources.Memory.MemoryMB 325 if mb < 128 || mb > 4000000 { 326 return nil, nil, fmt.Errorf("Qemu memory assignment out of bounds") 327 } 328 mem := fmt.Sprintf("%dM", mb) 329 330 absPath, err := GetAbsolutePath("qemu-system-x86_64") 331 if err != nil { 332 return nil, nil, err 333 } 334 335 args := []string{ 336 absPath, 337 "-machine", "type=pc,accel=" + accelerator, 338 "-name", vmID, 339 "-m", mem, 340 "-drive", "file=" + vmPath, 341 "-nographic", 342 } 343 344 var monitorPath string 345 if driverConfig.GracefulShutdown { 346 if runtime.GOOS == "windows" { 347 return nil, nil, errors.New("QEMU graceful shutdown is unsupported on the Windows platform") 348 } 349 // This socket will be used to manage the virtual machine (for example, 350 // to perform graceful shutdowns) 351 taskDir := filepath.Join(cfg.AllocDir, cfg.Name) 352 fingerPrint := d.buildFingerprint() 353 if fingerPrint.Attributes == nil { 354 return nil, nil, fmt.Errorf("unable to get qemu driver version from fingerprinted attributes") 355 } 356 monitorPath, err = d.getMonitorPath(taskDir, fingerPrint) 357 if err != nil { 358 d.logger.Debug("could not get qemu monitor path", "error", err) 359 return nil, nil, err 360 } 361 d.logger.Debug("got monitor path", "monitorPath", monitorPath) 362 args = append(args, "-monitor", fmt.Sprintf("unix:%s,server,nowait", monitorPath)) 363 } 364 365 // Add pass through arguments to qemu executable. A user can specify 366 // these arguments in driver task configuration. These arguments are 367 // passed directly to the qemu driver as command line options. 368 // For example, args = [ "-nodefconfig", "-nodefaults" ] 369 // This will allow a VM with embedded configuration to boot successfully. 370 args = append(args, driverConfig.Args...) 371 372 // Check the Resources required Networks to add port mappings. If no resources 373 // are required, we assume the VM is a purely compute job and does not require 374 // the outside world to be able to reach it. VMs ran without port mappings can 375 // still reach out to the world, but without port mappings it is effectively 376 // firewalled 377 protocols := []string{"udp", "tcp"} 378 if len(cfg.Resources.NomadResources.Networks) > 0 { 379 // Loop through the port map and construct the hostfwd string, to map 380 // reserved ports to the ports listenting in the VM 381 // Ex: hostfwd=tcp::22000-:22,hostfwd=tcp::80-:8080 382 var forwarding []string 383 taskPorts := cfg.Resources.NomadResources.Networks[0].PortLabels() 384 for label, guest := range driverConfig.PortMap { 385 host, ok := taskPorts[label] 386 if !ok { 387 return nil, nil, fmt.Errorf("Unknown port label %q", label) 388 } 389 390 for _, p := range protocols { 391 forwarding = append(forwarding, fmt.Sprintf("hostfwd=%s::%d-:%d", p, host, guest)) 392 } 393 } 394 395 if len(forwarding) != 0 { 396 args = append(args, 397 "-netdev", 398 fmt.Sprintf("user,id=user.0,%s", strings.Join(forwarding, ",")), 399 "-device", "virtio-net,netdev=user.0", 400 ) 401 } 402 } 403 404 // If using KVM, add optimization args 405 if accelerator == "kvm" { 406 if runtime.GOOS == "windows" { 407 return nil, nil, errors.New("KVM accelerator is unsupported on the Windows platform") 408 } 409 args = append(args, 410 "-enable-kvm", 411 "-cpu", "host", 412 // Do we have cores information available to the Driver? 413 // "-smp", fmt.Sprintf("%d", cores), 414 ) 415 } 416 d.logger.Debug("starting QemuVM command ", "args", strings.Join(args, " ")) 417 418 pluginLogFile := filepath.Join(cfg.TaskDir().Dir, fmt.Sprintf("%s-executor.out", cfg.Name)) 419 executorConfig := &executor.ExecutorConfig{ 420 LogFile: pluginLogFile, 421 LogLevel: "debug", 422 } 423 424 execImpl, pluginClient, err := executor.CreateExecutor( 425 d.logger.With("task_name", handle.Config.Name, "alloc_id", handle.Config.AllocID), 426 d.nomadConfig, executorConfig) 427 if err != nil { 428 return nil, nil, err 429 } 430 431 execCmd := &executor.ExecCommand{ 432 Cmd: args[0], 433 Args: args[1:], 434 Env: cfg.EnvList(), 435 User: cfg.User, 436 TaskDir: cfg.TaskDir().Dir, 437 StdoutPath: cfg.StdoutPath, 438 StderrPath: cfg.StderrPath, 439 } 440 ps, err := execImpl.Launch(execCmd) 441 if err != nil { 442 pluginClient.Kill() 443 return nil, nil, err 444 } 445 d.logger.Debug("started new QemuVM", "ID", vmID) 446 447 h := &taskHandle{ 448 exec: execImpl, 449 pid: ps.Pid, 450 monitorPath: monitorPath, 451 pluginClient: pluginClient, 452 taskConfig: cfg, 453 procState: drivers.TaskStateRunning, 454 startedAt: time.Now().Round(time.Millisecond), 455 logger: d.logger, 456 } 457 458 qemuDriverState := TaskState{ 459 ReattachConfig: pstructs.ReattachConfigFromGoPlugin(pluginClient.ReattachConfig()), 460 Pid: ps.Pid, 461 TaskConfig: cfg, 462 StartedAt: h.startedAt, 463 } 464 465 if err := handle.SetDriverState(&qemuDriverState); err != nil { 466 d.logger.Error("failed to start task, error setting driver state", "error", err) 467 execImpl.Shutdown("", 0) 468 pluginClient.Kill() 469 return nil, nil, fmt.Errorf("failed to set driver state: %v", err) 470 } 471 472 d.tasks.Set(cfg.ID, h) 473 go h.run() 474 475 var driverNetwork *drivers.DriverNetwork 476 if len(driverConfig.PortMap) == 1 { 477 driverNetwork = &drivers.DriverNetwork{ 478 PortMap: driverConfig.PortMap, 479 } 480 } 481 return handle, driverNetwork, nil 482 } 483 484 func (d *Driver) WaitTask(ctx context.Context, taskID string) (<-chan *drivers.ExitResult, error) { 485 handle, ok := d.tasks.Get(taskID) 486 if !ok { 487 return nil, drivers.ErrTaskNotFound 488 } 489 490 ch := make(chan *drivers.ExitResult) 491 go d.handleWait(ctx, handle, ch) 492 493 return ch, nil 494 } 495 496 func (d *Driver) StopTask(taskID string, timeout time.Duration, signal string) error { 497 handle, ok := d.tasks.Get(taskID) 498 if !ok { 499 return drivers.ErrTaskNotFound 500 } 501 502 // Attempt a graceful shutdown only if it was configured in the job 503 if handle.monitorPath != "" { 504 if err := sendQemuShutdown(d.logger, handle.monitorPath, handle.pid); err != nil { 505 d.logger.Debug("error sending graceful shutdown ", "pid", handle.pid, "error", err) 506 } 507 } 508 509 // TODO(preetha) we are calling shutdown on the executor here 510 // after attempting a graceful qemu shutdown, qemu process may 511 // not be around when we call exec.shutdown 512 if err := handle.exec.Shutdown(signal, timeout); err != nil { 513 if handle.pluginClient.Exited() { 514 return nil 515 } 516 return fmt.Errorf("executor Shutdown failed: %v", err) 517 } 518 519 return nil 520 } 521 522 func (d *Driver) DestroyTask(taskID string, force bool) error { 523 handle, ok := d.tasks.Get(taskID) 524 if !ok { 525 return drivers.ErrTaskNotFound 526 } 527 528 if handle.IsRunning() && !force { 529 return fmt.Errorf("cannot destroy running task") 530 } 531 532 if !handle.pluginClient.Exited() { 533 if handle.IsRunning() { 534 if err := handle.exec.Shutdown("", 0); err != nil { 535 handle.logger.Error("destroying executor failed", "err", err) 536 } 537 } 538 539 handle.pluginClient.Kill() 540 } 541 542 d.tasks.Delete(taskID) 543 return nil 544 } 545 546 func (d *Driver) InspectTask(taskID string) (*drivers.TaskStatus, error) { 547 handle, ok := d.tasks.Get(taskID) 548 if !ok { 549 return nil, drivers.ErrTaskNotFound 550 } 551 552 return handle.TaskStatus(), nil 553 } 554 555 func (d *Driver) TaskStats(ctx context.Context, taskID string, interval time.Duration) (<-chan *drivers.TaskResourceUsage, error) { 556 handle, ok := d.tasks.Get(taskID) 557 if !ok { 558 return nil, drivers.ErrTaskNotFound 559 } 560 561 return handle.exec.Stats(ctx, interval) 562 } 563 564 func (d *Driver) TaskEvents(ctx context.Context) (<-chan *drivers.TaskEvent, error) { 565 return d.eventer.TaskEvents(ctx) 566 } 567 568 func (d *Driver) SignalTask(taskID string, signal string) error { 569 return fmt.Errorf("Qemu driver can't signal commands") 570 } 571 572 func (d *Driver) ExecTask(taskID string, cmdArgs []string, timeout time.Duration) (*drivers.ExecTaskResult, error) { 573 return nil, fmt.Errorf("Qemu driver can't execute commands") 574 575 } 576 577 // GetAbsolutePath returns the absolute path of the passed binary by resolving 578 // it in the path and following symlinks. 579 func GetAbsolutePath(bin string) (string, error) { 580 lp, err := exec.LookPath(bin) 581 if err != nil { 582 return "", fmt.Errorf("failed to resolve path to %q executable: %v", bin, err) 583 } 584 585 return filepath.EvalSymlinks(lp) 586 } 587 588 func (d *Driver) handleWait(ctx context.Context, handle *taskHandle, ch chan *drivers.ExitResult) { 589 defer close(ch) 590 var result *drivers.ExitResult 591 ps, err := handle.exec.Wait(ctx) 592 if err != nil { 593 result = &drivers.ExitResult{ 594 Err: fmt.Errorf("executor: error waiting on process: %v", err), 595 } 596 } else { 597 result = &drivers.ExitResult{ 598 ExitCode: ps.ExitCode, 599 Signal: ps.Signal, 600 } 601 } 602 603 select { 604 case <-ctx.Done(): 605 case <-d.ctx.Done(): 606 case ch <- result: 607 } 608 } 609 610 // getMonitorPath is used to determine whether a qemu monitor socket can be 611 // safely created and accessed in the task directory by the version of qemu 612 // present on the host. If it is safe to use, the socket's full path is 613 // returned along with a nil error. Otherwise, an empty string is returned 614 // along with a descriptive error. 615 func (d *Driver) getMonitorPath(dir string, fingerPrint *drivers.Fingerprint) (string, error) { 616 var longPathSupport bool 617 currentQemuVer := fingerPrint.Attributes[driverVersionAttr] 618 currentQemuSemver := semver.New(currentQemuVer.GoString()) 619 if currentQemuSemver.LessThan(*qemuVersionLongSocketPathFix) { 620 longPathSupport = false 621 d.logger.Debug("long socket paths are not available in this version of QEMU", "version", currentQemuVer) 622 } else { 623 longPathSupport = true 624 d.logger.Debug("long socket paths available in this version of QEMU", "version", currentQemuVer) 625 } 626 fullSocketPath := fmt.Sprintf("%s/%s", dir, qemuMonitorSocketName) 627 if len(fullSocketPath) > qemuLegacyMaxMonitorPathLen && longPathSupport == false { 628 return "", fmt.Errorf("monitor path is too long for this version of qemu") 629 } 630 return fullSocketPath, nil 631 } 632 633 // sendQemuShutdown attempts to issue an ACPI power-off command via the qemu 634 // monitor 635 func sendQemuShutdown(logger hclog.Logger, monitorPath string, userPid int) error { 636 if monitorPath == "" { 637 return errors.New("monitorPath not set") 638 } 639 monitorSocket, err := net.Dial("unix", monitorPath) 640 if err != nil { 641 logger.Warn("could not connect to qemu monitor", "pid", userPid, "monitorPath", monitorPath, "error", err) 642 return err 643 } 644 defer monitorSocket.Close() 645 logger.Debug("sending graceful shutdown command to qemu monitor socket", "monitor_path", monitorPath, "pid", userPid) 646 _, err = monitorSocket.Write([]byte(qemuGracefulShutdownMsg)) 647 if err != nil { 648 logger.Warn("failed to send shutdown message", "shutdown message", qemuGracefulShutdownMsg, "monitorPath", monitorPath, "userPid", userPid, "error", err) 649 } 650 return err 651 } 652 653 func (d *Driver) Shutdown() { 654 d.signalShutdown() 655 }