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