github.com/anuvu/nomad@v0.8.7-atom1/client/driver/qemu.go (about) 1 package driver 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "log" 9 "net" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "regexp" 14 "runtime" 15 "strings" 16 "time" 17 18 "github.com/coreos/go-semver/semver" 19 plugin "github.com/hashicorp/go-plugin" 20 "github.com/hashicorp/nomad/client/driver/executor" 21 dstructs "github.com/hashicorp/nomad/client/driver/structs" 22 "github.com/hashicorp/nomad/client/fingerprint" 23 cstructs "github.com/hashicorp/nomad/client/structs" 24 "github.com/hashicorp/nomad/helper/fields" 25 "github.com/hashicorp/nomad/nomad/structs" 26 "github.com/mitchellh/mapstructure" 27 ) 28 29 var ( 30 reQemuVersion = regexp.MustCompile(`version (\d[\.\d+]+)`) 31 32 // Prior to qemu 2.10.1, monitor socket paths are truncated to 108 bytes. 33 // We should consider this if driver.qemu.version is < 2.10.1 and the 34 // generated monitor path is too long. 35 36 // 37 // Relevant fix is here: 38 // https://github.com/qemu/qemu/commit/ad9579aaa16d5b385922d49edac2c96c79bcfb6 39 qemuVersionLongSocketPathFix = semver.New("2.10.1") 40 ) 41 42 const ( 43 // The key populated in Node Attributes to indicate presence of the Qemu driver 44 qemuDriverAttr = "driver.qemu" 45 qemuDriverVersionAttr = "driver.qemu.version" 46 // Represents an ACPI shutdown request to the VM (emulates pressing a physical power button) 47 // Reference: https://en.wikibooks.org/wiki/QEMU/Monitor 48 qemuGracefulShutdownMsg = "system_powerdown\n" 49 qemuMonitorSocketName = "qemu-monitor.sock" 50 // Maximum socket path length prior to qemu 2.10.1 51 qemuLegacyMaxMonitorPathLen = 108 52 ) 53 54 // QemuDriver is a driver for running images via Qemu 55 // We attempt to chose sane defaults for now, with more configuration available 56 // planned in the future 57 type QemuDriver struct { 58 DriverContext 59 fingerprint.StaticFingerprinter 60 61 driverConfig *QemuDriverConfig 62 } 63 64 type QemuDriverConfig struct { 65 ImagePath string `mapstructure:"image_path"` 66 Accelerator string `mapstructure:"accelerator"` 67 GracefulShutdown bool `mapstructure:"graceful_shutdown"` 68 PortMap []map[string]int `mapstructure:"port_map"` // A map of host port labels and to guest ports. 69 Args []string `mapstructure:"args"` // extra arguments to qemu executable 70 } 71 72 // qemuHandle is returned from Start/Open as a handle to the PID 73 type qemuHandle struct { 74 pluginClient *plugin.Client 75 userPid int 76 executor executor.Executor 77 monitorPath string 78 killTimeout time.Duration 79 maxKillTimeout time.Duration 80 logger *log.Logger 81 version string 82 waitCh chan *dstructs.WaitResult 83 doneCh chan struct{} 84 } 85 86 // getMonitorPath is used to determine whether a qemu monitor socket can be 87 // safely created and accessed in the task directory by the version of qemu 88 // present on the host. If it is safe to use, the socket's full path is 89 // returned along with a nil error. Otherwise, an empty string is returned 90 // along with a descriptive error. 91 func (d *QemuDriver) getMonitorPath(dir string) (string, error) { 92 var longPathSupport bool 93 currentQemuVer := d.DriverContext.node.Attributes[qemuDriverVersionAttr] 94 currentQemuSemver := semver.New(currentQemuVer) 95 if currentQemuSemver.LessThan(*qemuVersionLongSocketPathFix) { 96 longPathSupport = false 97 d.logger.Printf("[DEBUG] driver.qemu: long socket paths are not available in this version of QEMU (%s)", currentQemuVer) 98 } else { 99 longPathSupport = true 100 d.logger.Printf("[DEBUG] driver.qemu: long socket paths available in this version of QEMU (%s)", currentQemuVer) 101 } 102 fullSocketPath := fmt.Sprintf("%s/%s", dir, qemuMonitorSocketName) 103 if len(fullSocketPath) > qemuLegacyMaxMonitorPathLen && longPathSupport == false { 104 return "", fmt.Errorf("monitor path is too long for this version of qemu") 105 } 106 return fullSocketPath, nil 107 } 108 109 // NewQemuDriver is used to create a new exec driver 110 func NewQemuDriver(ctx *DriverContext) Driver { 111 return &QemuDriver{DriverContext: *ctx} 112 } 113 114 // Validate is used to validate the driver configuration 115 func (d *QemuDriver) Validate(config map[string]interface{}) error { 116 fd := &fields.FieldData{ 117 Raw: config, 118 Schema: map[string]*fields.FieldSchema{ 119 "image_path": { 120 Type: fields.TypeString, 121 Required: true, 122 }, 123 "accelerator": { 124 Type: fields.TypeString, 125 }, 126 "graceful_shutdown": { 127 Type: fields.TypeBool, 128 Required: false, 129 }, 130 "port_map": { 131 Type: fields.TypeArray, 132 }, 133 "args": { 134 Type: fields.TypeArray, 135 }, 136 }, 137 } 138 139 if err := fd.Validate(); err != nil { 140 return err 141 } 142 143 return nil 144 } 145 146 func (d *QemuDriver) Abilities() DriverAbilities { 147 return DriverAbilities{ 148 SendSignals: false, 149 Exec: false, 150 } 151 } 152 153 func (d *QemuDriver) FSIsolation() cstructs.FSIsolation { 154 return cstructs.FSIsolationImage 155 } 156 157 func (d *QemuDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { 158 bin := "qemu-system-x86_64" 159 if runtime.GOOS == "windows" { 160 // On windows, the "qemu-system-x86_64" command does not respond to the 161 // version flag. 162 bin = "qemu-img" 163 } 164 outBytes, err := exec.Command(bin, "--version").Output() 165 if err != nil { 166 // return no error, as it isn't an error to not find qemu, it just means we 167 // can't use it. 168 return nil 169 } 170 out := strings.TrimSpace(string(outBytes)) 171 172 matches := reQemuVersion.FindStringSubmatch(out) 173 if len(matches) != 2 { 174 resp.RemoveAttribute(qemuDriverAttr) 175 return fmt.Errorf("Unable to parse Qemu version string: %#v", matches) 176 } 177 currentQemuVersion := matches[1] 178 179 resp.AddAttribute(qemuDriverAttr, "1") 180 resp.AddAttribute(qemuDriverVersionAttr, currentQemuVersion) 181 resp.Detected = true 182 183 return nil 184 } 185 186 func (d *QemuDriver) Prestart(_ *ExecContext, task *structs.Task) (*PrestartResponse, error) { 187 var driverConfig QemuDriverConfig 188 if err := mapstructure.WeakDecode(task.Config, &driverConfig); err != nil { 189 return nil, err 190 } 191 192 if len(driverConfig.PortMap) > 1 { 193 return nil, fmt.Errorf("Only one port_map block is allowed in the qemu driver config") 194 } 195 196 d.driverConfig = &driverConfig 197 198 r := NewPrestartResponse() 199 if len(driverConfig.PortMap) == 1 { 200 r.Network = &cstructs.DriverNetwork{ 201 PortMap: driverConfig.PortMap[0], 202 } 203 } 204 return r, nil 205 } 206 207 // Run an existing Qemu image. Start() will pull down an existing, valid Qemu 208 // image and save it to the Drivers Allocation Dir 209 func (d *QemuDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, error) { 210 // Get the image source 211 vmPath := d.driverConfig.ImagePath 212 if vmPath == "" { 213 return nil, fmt.Errorf("image_path must be set") 214 } 215 vmID := filepath.Base(vmPath) 216 217 // Parse configuration arguments 218 // Create the base arguments 219 accelerator := "tcg" 220 if d.driverConfig.Accelerator != "" { 221 accelerator = d.driverConfig.Accelerator 222 } 223 224 if task.Resources.MemoryMB < 128 || task.Resources.MemoryMB > 4000000 { 225 return nil, fmt.Errorf("Qemu memory assignment out of bounds") 226 } 227 mem := fmt.Sprintf("%dM", task.Resources.MemoryMB) 228 229 absPath, err := GetAbsolutePath("qemu-system-x86_64") 230 if err != nil { 231 return nil, err 232 } 233 234 args := []string{ 235 absPath, 236 "-machine", "type=pc,accel=" + accelerator, 237 "-name", vmID, 238 "-m", mem, 239 "-drive", "file=" + vmPath, 240 "-nographic", 241 } 242 243 var monitorPath string 244 if d.driverConfig.GracefulShutdown { 245 if runtime.GOOS == "windows" { 246 return nil, errors.New("QEMU graceful shutdown is unsupported on the Windows platform") 247 } 248 // This socket will be used to manage the virtual machine (for example, 249 // to perform graceful shutdowns) 250 monitorPath, err = d.getMonitorPath(ctx.TaskDir.Dir) 251 if err != nil { 252 d.logger.Printf("[ERR] driver.qemu: could not get qemu monitor path: %s", err) 253 return nil, err 254 } 255 d.logger.Printf("[DEBUG] driver.qemu: got monitor path OK: %s", monitorPath) 256 args = append(args, "-monitor", fmt.Sprintf("unix:%s,server,nowait", monitorPath)) 257 } 258 259 // Add pass through arguments to qemu executable. A user can specify 260 // these arguments in driver task configuration. These arguments are 261 // passed directly to the qemu driver as command line options. 262 // For example, args = [ "-nodefconfig", "-nodefaults" ] 263 // This will allow a VM with embedded configuration to boot successfully. 264 args = append(args, d.driverConfig.Args...) 265 266 // Check the Resources required Networks to add port mappings. If no resources 267 // are required, we assume the VM is a purely compute job and does not require 268 // the outside world to be able to reach it. VMs ran without port mappings can 269 // still reach out to the world, but without port mappings it is effectively 270 // firewalled 271 protocols := []string{"udp", "tcp"} 272 if len(task.Resources.Networks) > 0 && len(d.driverConfig.PortMap) == 1 { 273 // Loop through the port map and construct the hostfwd string, to map 274 // reserved ports to the ports listenting in the VM 275 // Ex: hostfwd=tcp::22000-:22,hostfwd=tcp::80-:8080 276 var forwarding []string 277 taskPorts := task.Resources.Networks[0].PortLabels() 278 for label, guest := range d.driverConfig.PortMap[0] { 279 host, ok := taskPorts[label] 280 if !ok { 281 return nil, fmt.Errorf("Unknown port label %q", label) 282 } 283 284 for _, p := range protocols { 285 forwarding = append(forwarding, fmt.Sprintf("hostfwd=%s::%d-:%d", p, host, guest)) 286 } 287 } 288 289 if len(forwarding) != 0 { 290 args = append(args, 291 "-netdev", 292 fmt.Sprintf("user,id=user.0,%s", strings.Join(forwarding, ",")), 293 "-device", "virtio-net,netdev=user.0", 294 ) 295 } 296 } 297 298 // If using KVM, add optimization args 299 if accelerator == "kvm" { 300 if runtime.GOOS == "windows" { 301 return nil, errors.New("KVM accelerator is unsupported on the Windows platform") 302 } 303 args = append(args, 304 "-enable-kvm", 305 "-cpu", "host", 306 // Do we have cores information available to the Driver? 307 // "-smp", fmt.Sprintf("%d", cores), 308 ) 309 } 310 311 d.logger.Printf("[DEBUG] driver.qemu: starting QemuVM command: %q", strings.Join(args, " ")) 312 pluginLogFile := filepath.Join(ctx.TaskDir.Dir, "executor.out") 313 executorConfig := &dstructs.ExecutorConfig{ 314 LogFile: pluginLogFile, 315 LogLevel: d.config.LogLevel, 316 } 317 318 exec, pluginClient, err := createExecutor(d.config.LogOutput, d.config, executorConfig) 319 if err != nil { 320 return nil, err 321 } 322 executorCtx := &executor.ExecutorContext{ 323 TaskEnv: ctx.TaskEnv, 324 Driver: "qemu", 325 Task: task, 326 TaskDir: ctx.TaskDir.Dir, 327 LogDir: ctx.TaskDir.LogDir, 328 } 329 if err := exec.SetContext(executorCtx); err != nil { 330 pluginClient.Kill() 331 return nil, fmt.Errorf("failed to set executor context: %v", err) 332 } 333 334 execCmd := &executor.ExecCommand{ 335 Cmd: args[0], 336 Args: args[1:], 337 User: task.User, 338 } 339 ps, err := exec.LaunchCmd(execCmd) 340 if err != nil { 341 pluginClient.Kill() 342 return nil, err 343 } 344 d.logger.Printf("[INFO] driver.qemu: started new QemuVM: %s", vmID) 345 346 // Create and Return Handle 347 maxKill := d.DriverContext.config.MaxKillTimeout 348 h := &qemuHandle{ 349 pluginClient: pluginClient, 350 executor: exec, 351 userPid: ps.Pid, 352 killTimeout: GetKillTimeout(task.KillTimeout, maxKill), 353 maxKillTimeout: maxKill, 354 monitorPath: monitorPath, 355 version: d.config.Version.VersionNumber(), 356 logger: d.logger, 357 doneCh: make(chan struct{}), 358 waitCh: make(chan *dstructs.WaitResult, 1), 359 } 360 go h.run() 361 resp := &StartResponse{Handle: h} 362 if len(d.driverConfig.PortMap) == 1 { 363 resp.Network = &cstructs.DriverNetwork{ 364 PortMap: d.driverConfig.PortMap[0], 365 } 366 } 367 return resp, nil 368 } 369 370 type qemuId struct { 371 Version string 372 KillTimeout time.Duration 373 MaxKillTimeout time.Duration 374 UserPid int 375 PluginConfig *PluginReattachConfig 376 } 377 378 func (d *QemuDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, error) { 379 id := &qemuId{} 380 if err := json.Unmarshal([]byte(handleID), id); err != nil { 381 return nil, fmt.Errorf("Failed to parse handle %q: %v", handleID, err) 382 } 383 384 pluginConfig := &plugin.ClientConfig{ 385 Reattach: id.PluginConfig.PluginConfig(), 386 } 387 388 exec, pluginClient, err := createExecutorWithConfig(pluginConfig, d.config.LogOutput) 389 if err != nil { 390 d.logger.Printf("[ERR] driver.qemu: error connecting to plugin so destroying plugin pid %d and user pid %d", id.PluginConfig.Pid, id.UserPid) 391 if e := destroyPlugin(id.PluginConfig.Pid, id.UserPid); e != nil { 392 d.logger.Printf("[ERR] driver.qemu: error destroying plugin pid %d and userpid %d: %v", id.PluginConfig.Pid, id.UserPid, e) 393 } 394 return nil, fmt.Errorf("error connecting to plugin: %v", err) 395 } 396 397 ver, _ := exec.Version() 398 d.logger.Printf("[DEBUG] driver.qemu: version of executor: %v", ver.Version) 399 // Return a driver handle 400 h := &qemuHandle{ 401 pluginClient: pluginClient, 402 executor: exec, 403 userPid: id.UserPid, 404 logger: d.logger, 405 killTimeout: id.KillTimeout, 406 maxKillTimeout: id.MaxKillTimeout, 407 version: id.Version, 408 doneCh: make(chan struct{}), 409 waitCh: make(chan *dstructs.WaitResult, 1), 410 } 411 go h.run() 412 return h, nil 413 } 414 415 func (d *QemuDriver) Cleanup(*ExecContext, *CreatedResources) error { return nil } 416 417 func (h *qemuHandle) ID() string { 418 id := qemuId{ 419 Version: h.version, 420 KillTimeout: h.killTimeout, 421 MaxKillTimeout: h.maxKillTimeout, 422 PluginConfig: NewPluginReattachConfig(h.pluginClient.ReattachConfig()), 423 UserPid: h.userPid, 424 } 425 426 data, err := json.Marshal(id) 427 if err != nil { 428 h.logger.Printf("[ERR] driver.qemu: failed to marshal ID to JSON: %s", err) 429 } 430 return string(data) 431 } 432 433 func (h *qemuHandle) WaitCh() chan *dstructs.WaitResult { 434 return h.waitCh 435 } 436 437 func (h *qemuHandle) Update(task *structs.Task) error { 438 // Store the updated kill timeout. 439 h.killTimeout = GetKillTimeout(task.KillTimeout, h.maxKillTimeout) 440 h.executor.UpdateTask(task) 441 442 // Update is not possible 443 return nil 444 } 445 446 func (h *qemuHandle) Exec(ctx context.Context, cmd string, args []string) ([]byte, int, error) { 447 return nil, 0, fmt.Errorf("Qemu driver can't execute commands") 448 } 449 450 func (h *qemuHandle) Signal(s os.Signal) error { 451 return fmt.Errorf("Qemu driver can't send signals") 452 } 453 454 func (h *qemuHandle) Kill() error { 455 gracefulShutdownSent := false 456 // Attempt a graceful shutdown only if it was configured in the job 457 if h.monitorPath != "" { 458 if err := sendQemuShutdown(h.logger, h.monitorPath, h.userPid); err == nil { 459 gracefulShutdownSent = true 460 } else { 461 h.logger.Printf("[DEBUG] driver.qemu: error sending graceful shutdown for user process pid %d: %s", h.userPid, err) 462 } 463 } 464 465 // If Nomad did not send a graceful shutdown signal, issue an interrupt to 466 // the qemu process as a last resort 467 if gracefulShutdownSent == false { 468 h.logger.Printf("[DEBUG] driver.qemu: graceful shutdown is not enabled, sending an interrupt signal to pid: %d", h.userPid) 469 if err := h.executor.ShutDown(); err != nil { 470 if h.pluginClient.Exited() { 471 return nil 472 } 473 return fmt.Errorf("executor Shutdown failed: %v", err) 474 } 475 } 476 477 // If the qemu process exits before the kill timeout is reached, doneChan 478 // will close and we'll exit without an error. If it takes too long, the 479 // timer will fire and we'll attempt to kill the process. 480 select { 481 case <-h.doneCh: 482 return nil 483 case <-time.After(h.killTimeout): 484 h.logger.Printf("[DEBUG] driver.qemu: kill timeout of %s exceeded for user process pid %d", h.killTimeout.String(), h.userPid) 485 486 if h.pluginClient.Exited() { 487 return nil 488 } 489 if err := h.executor.Exit(); err != nil { 490 return fmt.Errorf("executor Exit failed: %v", err) 491 } 492 return nil 493 } 494 } 495 496 func (h *qemuHandle) Stats() (*cstructs.TaskResourceUsage, error) { 497 return h.executor.Stats() 498 } 499 500 func (h *qemuHandle) run() { 501 ps, werr := h.executor.Wait() 502 if ps.ExitCode == 0 && werr != nil { 503 if e := killProcess(h.userPid); e != nil { 504 h.logger.Printf("[ERR] driver.qemu: error killing user process pid %d: %v", h.userPid, e) 505 } 506 } 507 close(h.doneCh) 508 509 // Exit the executor 510 h.executor.Exit() 511 h.pluginClient.Kill() 512 513 // Send the results 514 h.waitCh <- &dstructs.WaitResult{ExitCode: ps.ExitCode, Signal: ps.Signal, Err: werr} 515 close(h.waitCh) 516 } 517 518 // sendQemuShutdown attempts to issue an ACPI power-off command via the qemu 519 // monitor 520 func sendQemuShutdown(logger *log.Logger, monitorPath string, userPid int) error { 521 if monitorPath == "" { 522 return errors.New("monitorPath not set") 523 } 524 monitorSocket, err := net.Dial("unix", monitorPath) 525 if err != nil { 526 logger.Printf("[WARN] driver.qemu: could not connect to qemu monitor %q for user process pid %d: %s", monitorPath, userPid, err) 527 return err 528 } 529 defer monitorSocket.Close() 530 logger.Printf("[DEBUG] driver.qemu: sending graceful shutdown command to qemu monitor socket %q for user process pid %d", monitorPath, userPid) 531 _, err = monitorSocket.Write([]byte(qemuGracefulShutdownMsg)) 532 if err != nil { 533 logger.Printf("[WARN] driver.qemu: failed to send shutdown message %q to monitor socket %q for user process pid %d: %s", qemuGracefulShutdownMsg, monitorPath, userPid, err) 534 } 535 return err 536 }