github.com/maier/nomad@v0.4.1-0.20161110003312-a9e3d0b8549d/client/driver/qemu.go (about) 1 package driver 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "log" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "regexp" 11 "runtime" 12 "strings" 13 "time" 14 15 "github.com/hashicorp/go-plugin" 16 "github.com/hashicorp/nomad/client/allocdir" 17 "github.com/hashicorp/nomad/client/config" 18 "github.com/hashicorp/nomad/client/driver/executor" 19 dstructs "github.com/hashicorp/nomad/client/driver/structs" 20 "github.com/hashicorp/nomad/client/fingerprint" 21 cstructs "github.com/hashicorp/nomad/client/structs" 22 "github.com/hashicorp/nomad/helper/discover" 23 "github.com/hashicorp/nomad/helper/fields" 24 "github.com/hashicorp/nomad/nomad/structs" 25 "github.com/mitchellh/mapstructure" 26 ) 27 28 var ( 29 reQemuVersion = regexp.MustCompile(`version (\d[\.\d+]+)`) 30 ) 31 32 const ( 33 // The key populated in Node Attributes to indicate presence of the Qemu 34 // driver 35 qemuDriverAttr = "driver.qemu" 36 ) 37 38 // QemuDriver is a driver for running images via Qemu 39 // We attempt to chose sane defaults for now, with more configuration available 40 // planned in the future 41 type QemuDriver struct { 42 DriverContext 43 fingerprint.StaticFingerprinter 44 } 45 46 type QemuDriverConfig struct { 47 ImagePath string `mapstructure:"image_path"` 48 Accelerator string `mapstructure:"accelerator"` 49 PortMap []map[string]int `mapstructure:"port_map"` // A map of host port labels and to guest ports. 50 Args []string `mapstructure:"args"` // extra arguments to qemu executable 51 } 52 53 // qemuHandle is returned from Start/Open as a handle to the PID 54 type qemuHandle struct { 55 pluginClient *plugin.Client 56 userPid int 57 executor executor.Executor 58 allocDir *allocdir.AllocDir 59 killTimeout time.Duration 60 maxKillTimeout time.Duration 61 logger *log.Logger 62 version string 63 waitCh chan *dstructs.WaitResult 64 doneCh chan struct{} 65 } 66 67 // NewQemuDriver is used to create a new exec driver 68 func NewQemuDriver(ctx *DriverContext) Driver { 69 return &QemuDriver{DriverContext: *ctx} 70 } 71 72 // Validate is used to validate the driver configuration 73 func (d *QemuDriver) Validate(config map[string]interface{}) error { 74 fd := &fields.FieldData{ 75 Raw: config, 76 Schema: map[string]*fields.FieldSchema{ 77 "image_path": &fields.FieldSchema{ 78 Type: fields.TypeString, 79 Required: true, 80 }, 81 "accelerator": &fields.FieldSchema{ 82 Type: fields.TypeString, 83 }, 84 "port_map": &fields.FieldSchema{ 85 Type: fields.TypeArray, 86 }, 87 "args": &fields.FieldSchema{ 88 Type: fields.TypeArray, 89 }, 90 }, 91 } 92 93 if err := fd.Validate(); err != nil { 94 return err 95 } 96 97 return nil 98 } 99 100 func (d *QemuDriver) Abilities() DriverAbilities { 101 return DriverAbilities{ 102 SendSignals: false, 103 } 104 } 105 106 func (d *QemuDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { 107 // Get the current status so that we can log any debug messages only if the 108 // state changes 109 _, currentlyEnabled := node.Attributes[qemuDriverAttr] 110 111 bin := "qemu-system-x86_64" 112 if runtime.GOOS == "windows" { 113 // On windows, the "qemu-system-x86_64" command does not respond to the 114 // version flag. 115 bin = "qemu-img" 116 } 117 outBytes, err := exec.Command(bin, "--version").Output() 118 if err != nil { 119 delete(node.Attributes, qemuDriverAttr) 120 return false, nil 121 } 122 out := strings.TrimSpace(string(outBytes)) 123 124 matches := reQemuVersion.FindStringSubmatch(out) 125 if len(matches) != 2 { 126 delete(node.Attributes, qemuDriverAttr) 127 return false, fmt.Errorf("Unable to parse Qemu version string: %#v", matches) 128 } 129 130 if !currentlyEnabled { 131 d.logger.Printf("[DEBUG] driver.qemu: enabling driver") 132 } 133 node.Attributes[qemuDriverAttr] = "1" 134 node.Attributes["driver.qemu.version"] = matches[1] 135 return true, nil 136 } 137 138 // Run an existing Qemu image. Start() will pull down an existing, valid Qemu 139 // image and save it to the Drivers Allocation Dir 140 func (d *QemuDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) { 141 var driverConfig QemuDriverConfig 142 if err := mapstructure.WeakDecode(task.Config, &driverConfig); err != nil { 143 return nil, err 144 } 145 146 if len(driverConfig.PortMap) > 1 { 147 return nil, fmt.Errorf("Only one port_map block is allowed in the qemu driver config") 148 } 149 150 // Get the image source 151 vmPath := driverConfig.ImagePath 152 if vmPath == "" { 153 return nil, fmt.Errorf("image_path must be set") 154 } 155 vmID := filepath.Base(vmPath) 156 157 // Get the tasks local directory. 158 taskDir, ok := ctx.AllocDir.TaskDirs[d.DriverContext.taskName] 159 if !ok { 160 return nil, fmt.Errorf("Could not find task directory for task: %v", d.DriverContext.taskName) 161 } 162 163 // Parse configuration arguments 164 // Create the base arguments 165 accelerator := "tcg" 166 if driverConfig.Accelerator != "" { 167 accelerator = driverConfig.Accelerator 168 } 169 // TODO: Check a lower bounds, e.g. the default 128 of Qemu 170 mem := fmt.Sprintf("%dM", task.Resources.MemoryMB) 171 172 absPath, err := GetAbsolutePath("qemu-system-x86_64") 173 if err != nil { 174 return nil, err 175 } 176 177 args := []string{ 178 absPath, 179 "-machine", "type=pc,accel=" + accelerator, 180 "-name", vmID, 181 "-m", mem, 182 "-drive", "file=" + vmPath, 183 "-nographic", 184 } 185 186 // Add pass through arguments to qemu executable. A user can specify 187 // these arguments in driver task configuration. These arguments are 188 // passed directly to the qemu driver as command line options. 189 // For example, args = [ "-nodefconfig", "-nodefaults" ] 190 // This will allow a VM with embedded configuration to boot successfully. 191 args = append(args, driverConfig.Args...) 192 193 // Check the Resources required Networks to add port mappings. If no resources 194 // are required, we assume the VM is a purely compute job and does not require 195 // the outside world to be able to reach it. VMs ran without port mappings can 196 // still reach out to the world, but without port mappings it is effectively 197 // firewalled 198 protocols := []string{"udp", "tcp"} 199 if len(task.Resources.Networks) > 0 && len(driverConfig.PortMap) == 1 { 200 // Loop through the port map and construct the hostfwd string, to map 201 // reserved ports to the ports listenting in the VM 202 // Ex: hostfwd=tcp::22000-:22,hostfwd=tcp::80-:8080 203 var forwarding []string 204 taskPorts := task.Resources.Networks[0].MapLabelToValues(nil) 205 for label, guest := range driverConfig.PortMap[0] { 206 host, ok := taskPorts[label] 207 if !ok { 208 return nil, fmt.Errorf("Unknown port label %q", label) 209 } 210 211 for _, p := range protocols { 212 forwarding = append(forwarding, fmt.Sprintf("hostfwd=%s::%d-:%d", p, host, guest)) 213 } 214 } 215 216 if len(forwarding) != 0 { 217 args = append(args, 218 "-netdev", 219 fmt.Sprintf("user,id=user.0,%s", strings.Join(forwarding, ",")), 220 "-device", "virtio-net,netdev=user.0", 221 ) 222 } 223 } 224 225 // If using KVM, add optimization args 226 if accelerator == "kvm" { 227 args = append(args, 228 "-enable-kvm", 229 "-cpu", "host", 230 // Do we have cores information available to the Driver? 231 // "-smp", fmt.Sprintf("%d", cores), 232 ) 233 } 234 235 d.logger.Printf("[DEBUG] Starting QemuVM command: %q", strings.Join(args, " ")) 236 bin, err := discover.NomadExecutable() 237 if err != nil { 238 return nil, fmt.Errorf("unable to find the nomad binary: %v", err) 239 } 240 241 pluginLogFile := filepath.Join(taskDir, fmt.Sprintf("%s-executor.out", task.Name)) 242 pluginConfig := &plugin.ClientConfig{ 243 Cmd: exec.Command(bin, "executor", pluginLogFile), 244 } 245 246 exec, pluginClient, err := createExecutor(pluginConfig, d.config.LogOutput, d.config) 247 if err != nil { 248 return nil, err 249 } 250 executorCtx := &executor.ExecutorContext{ 251 TaskEnv: d.taskEnv, 252 Driver: "qemu", 253 AllocDir: ctx.AllocDir, 254 AllocID: ctx.AllocID, 255 Task: task, 256 } 257 if err := exec.SetContext(executorCtx); err != nil { 258 pluginClient.Kill() 259 return nil, fmt.Errorf("failed to set executor context: %v", err) 260 } 261 262 execCmd := &executor.ExecCommand{ 263 Cmd: args[0], 264 Args: args[1:], 265 User: task.User, 266 } 267 ps, err := exec.LaunchCmd(execCmd) 268 if err != nil { 269 pluginClient.Kill() 270 return nil, err 271 } 272 d.logger.Printf("[INFO] Started new QemuVM: %s", vmID) 273 274 // Create and Return Handle 275 maxKill := d.DriverContext.config.MaxKillTimeout 276 h := &qemuHandle{ 277 pluginClient: pluginClient, 278 executor: exec, 279 userPid: ps.Pid, 280 allocDir: ctx.AllocDir, 281 killTimeout: GetKillTimeout(task.KillTimeout, maxKill), 282 maxKillTimeout: maxKill, 283 version: d.config.Version, 284 logger: d.logger, 285 doneCh: make(chan struct{}), 286 waitCh: make(chan *dstructs.WaitResult, 1), 287 } 288 289 if err := h.executor.SyncServices(consulContext(d.config, "")); err != nil { 290 h.logger.Printf("[ERR] driver.qemu: error registering services for task: %q: %v", task.Name, err) 291 } 292 go h.run() 293 return h, nil 294 } 295 296 type qemuId struct { 297 Version string 298 KillTimeout time.Duration 299 MaxKillTimeout time.Duration 300 UserPid int 301 PluginConfig *PluginReattachConfig 302 AllocDir *allocdir.AllocDir 303 } 304 305 func (d *QemuDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, error) { 306 id := &qemuId{} 307 if err := json.Unmarshal([]byte(handleID), id); err != nil { 308 return nil, fmt.Errorf("Failed to parse handle '%s': %v", handleID, err) 309 } 310 311 pluginConfig := &plugin.ClientConfig{ 312 Reattach: id.PluginConfig.PluginConfig(), 313 } 314 315 exec, pluginClient, err := createExecutor(pluginConfig, d.config.LogOutput, d.config) 316 if err != nil { 317 d.logger.Println("[ERR] driver.qemu: error connecting to plugin so destroying plugin pid and user pid") 318 if e := destroyPlugin(id.PluginConfig.Pid, id.UserPid); e != nil { 319 d.logger.Printf("[ERR] driver.qemu: error destroying plugin and userpid: %v", e) 320 } 321 return nil, fmt.Errorf("error connecting to plugin: %v", err) 322 } 323 324 ver, _ := exec.Version() 325 d.logger.Printf("[DEBUG] driver.qemu: version of executor: %v", ver.Version) 326 // Return a driver handle 327 h := &qemuHandle{ 328 pluginClient: pluginClient, 329 executor: exec, 330 userPid: id.UserPid, 331 allocDir: id.AllocDir, 332 logger: d.logger, 333 killTimeout: id.KillTimeout, 334 maxKillTimeout: id.MaxKillTimeout, 335 version: id.Version, 336 doneCh: make(chan struct{}), 337 waitCh: make(chan *dstructs.WaitResult, 1), 338 } 339 if err := h.executor.SyncServices(consulContext(d.config, "")); err != nil { 340 h.logger.Printf("[ERR] driver.qemu: error registering services: %v", err) 341 } 342 go h.run() 343 return h, nil 344 } 345 346 func (h *qemuHandle) ID() string { 347 id := qemuId{ 348 Version: h.version, 349 KillTimeout: h.killTimeout, 350 MaxKillTimeout: h.maxKillTimeout, 351 PluginConfig: NewPluginReattachConfig(h.pluginClient.ReattachConfig()), 352 UserPid: h.userPid, 353 AllocDir: h.allocDir, 354 } 355 356 data, err := json.Marshal(id) 357 if err != nil { 358 h.logger.Printf("[ERR] driver.qemu: failed to marshal ID to JSON: %s", err) 359 } 360 return string(data) 361 } 362 363 func (h *qemuHandle) WaitCh() chan *dstructs.WaitResult { 364 return h.waitCh 365 } 366 367 func (h *qemuHandle) Update(task *structs.Task) error { 368 // Store the updated kill timeout. 369 h.killTimeout = GetKillTimeout(task.KillTimeout, h.maxKillTimeout) 370 h.executor.UpdateTask(task) 371 372 // Update is not possible 373 return nil 374 } 375 376 func (h *qemuHandle) Signal(s os.Signal) error { 377 return fmt.Errorf("Qemu driver can't send signals") 378 } 379 380 // TODO: allow a 'shutdown_command' that can be executed over a ssh connection 381 // to the VM 382 func (h *qemuHandle) Kill() error { 383 if err := h.executor.ShutDown(); err != nil { 384 if h.pluginClient.Exited() { 385 return nil 386 } 387 return fmt.Errorf("executor Shutdown failed: %v", err) 388 } 389 390 select { 391 case <-h.doneCh: 392 return nil 393 case <-time.After(h.killTimeout): 394 if h.pluginClient.Exited() { 395 return nil 396 } 397 if err := h.executor.Exit(); err != nil { 398 return fmt.Errorf("executor Exit failed: %v", err) 399 } 400 401 return nil 402 } 403 } 404 405 func (h *qemuHandle) Stats() (*cstructs.TaskResourceUsage, error) { 406 return h.executor.Stats() 407 } 408 409 func (h *qemuHandle) run() { 410 ps, werr := h.executor.Wait() 411 if ps.ExitCode == 0 && werr != nil { 412 if e := killProcess(h.userPid); e != nil { 413 h.logger.Printf("[ERR] driver.qemu: error killing user process: %v", e) 414 } 415 if e := h.allocDir.UnmountAll(); e != nil { 416 h.logger.Printf("[ERR] driver.qemu: unmounting dev,proc and alloc dirs failed: %v", e) 417 } 418 } 419 close(h.doneCh) 420 421 // Remove services 422 if err := h.executor.DeregisterServices(); err != nil { 423 h.logger.Printf("[ERR] driver.qemu: failed to deregister services: %v", err) 424 } 425 426 // Exit the executor 427 h.executor.Exit() 428 h.pluginClient.Kill() 429 430 // Send the results 431 h.waitCh <- &dstructs.WaitResult{ExitCode: ps.ExitCode, Signal: ps.Signal, Err: werr} 432 close(h.waitCh) 433 }