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