github.com/kardianos/nomad@v0.1.3-0.20151022182107-b13df73ee850/client/driver/qemu.go (about) 1 package driver 2 3 import ( 4 "bytes" 5 "crypto/sha256" 6 "encoding/hex" 7 "encoding/json" 8 "fmt" 9 "io" 10 "log" 11 "net/http" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "regexp" 16 "runtime" 17 "strconv" 18 "strings" 19 "syscall" 20 "time" 21 22 "github.com/hashicorp/nomad/client/allocdir" 23 "github.com/hashicorp/nomad/client/config" 24 "github.com/hashicorp/nomad/nomad/structs" 25 ) 26 27 var ( 28 reQemuVersion = regexp.MustCompile("QEMU emulator version ([\\d\\.]+).+") 29 ) 30 31 // QemuDriver is a driver for running images via Qemu 32 // We attempt to chose sane defaults for now, with more configuration available 33 // planned in the future 34 type QemuDriver struct { 35 DriverContext 36 } 37 38 // qemuHandle is returned from Start/Open as a handle to the PID 39 type qemuHandle struct { 40 proc *os.Process 41 vmID string 42 waitCh chan error 43 doneCh chan struct{} 44 } 45 46 // qemuPID is a struct to map the pid running the process to the vm image on 47 // disk 48 type qemuPID struct { 49 Pid int 50 VmID string 51 } 52 53 // NewQemuDriver is used to create a new exec driver 54 func NewQemuDriver(ctx *DriverContext) Driver { 55 return &QemuDriver{*ctx} 56 } 57 58 func (d *QemuDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { 59 // Only enable if we are root when running on non-windows systems. 60 if runtime.GOOS != "windows" && syscall.Geteuid() != 0 { 61 d.logger.Printf("[DEBUG] driver.qemu: must run as root user, disabling") 62 return false, nil 63 } 64 65 outBytes, err := exec.Command("qemu-system-x86_64", "-version").Output() 66 if err != nil { 67 return false, nil 68 } 69 out := strings.TrimSpace(string(outBytes)) 70 71 matches := reQemuVersion.FindStringSubmatch(out) 72 if len(matches) != 2 { 73 return false, fmt.Errorf("Unable to parse Qemu version string: %#v", matches) 74 } 75 76 node.Attributes["driver.qemu"] = "1" 77 node.Attributes["driver.qemu.version"] = matches[1] 78 79 return true, nil 80 } 81 82 // Run an existing Qemu image. Start() will pull down an existing, valid Qemu 83 // image and save it to the Drivers Allocation Dir 84 func (d *QemuDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) { 85 // Get the image source 86 source, ok := task.Config["image_source"] 87 if !ok || source == "" { 88 return nil, fmt.Errorf("Missing source image Qemu driver") 89 } 90 91 // Qemu defaults to 128M of RAM for a given VM. Instead, we force users to 92 // supply a memory size in the tasks resources 93 if task.Resources == nil || task.Resources.MemoryMB == 0 { 94 return nil, fmt.Errorf("Missing required Task Resource: Memory") 95 } 96 97 // Attempt to download the thing 98 // Should be extracted to some kind of Http Fetcher 99 // Right now, assume publicly accessible HTTP url 100 resp, err := http.Get(source) 101 if err != nil { 102 return nil, fmt.Errorf("Error downloading source for Qemu driver: %s", err) 103 } 104 105 // Get the tasks local directory. 106 taskDir, ok := ctx.AllocDir.TaskDirs[d.DriverContext.taskName] 107 if !ok { 108 return nil, fmt.Errorf("Could not find task directory for task: %v", d.DriverContext.taskName) 109 } 110 taskLocal := filepath.Join(taskDir, allocdir.TaskLocal) 111 112 // Create a location in the local directory to download and store the image. 113 // TODO: Caching 114 vmID := fmt.Sprintf("qemu-vm-%s-%s", structs.GenerateUUID(), filepath.Base(source)) 115 fPath := filepath.Join(taskLocal, vmID) 116 vmPath, err := os.OpenFile(fPath, os.O_CREATE|os.O_WRONLY, 0666) 117 if err != nil { 118 return nil, fmt.Errorf("Error opening file to download to: %s", err) 119 } 120 121 defer vmPath.Close() 122 defer resp.Body.Close() 123 124 // Copy remote file to local AllocDir for execution 125 // TODO: a retry of sort if io.Copy fails, for large binaries 126 _, ioErr := io.Copy(vmPath, resp.Body) 127 if ioErr != nil { 128 return nil, fmt.Errorf("Error copying Qemu image from source: %s", ioErr) 129 } 130 131 // compute and check checksum 132 if check, ok := task.Config["checksum"]; ok { 133 d.logger.Printf("[DEBUG] Running checksum on (%s)", vmID) 134 hasher := sha256.New() 135 file, err := os.Open(vmPath.Name()) 136 if err != nil { 137 return nil, fmt.Errorf("Failed to open file for checksum") 138 } 139 140 defer file.Close() 141 io.Copy(hasher, file) 142 143 sum := hex.EncodeToString(hasher.Sum(nil)) 144 if sum != check { 145 return nil, fmt.Errorf( 146 "Error in Qemu: checksums did not match.\nExpected (%s), got (%s)", 147 check, 148 sum) 149 } 150 } 151 152 // Parse configuration arguments 153 // Create the base arguments 154 accelerator := "tcg" 155 if acc, ok := task.Config["accelerator"]; ok { 156 accelerator = acc 157 } 158 // TODO: Check a lower bounds, e.g. the default 128 of Qemu 159 mem := fmt.Sprintf("%dM", task.Resources.MemoryMB) 160 161 args := []string{ 162 "qemu-system-x86_64", 163 "-machine", "type=pc,accel=" + accelerator, 164 "-name", vmID, 165 "-m", mem, 166 "-drive", "file=" + vmPath.Name(), 167 "-nodefconfig", 168 "-nodefaults", 169 "-nographic", 170 } 171 172 // Check the Resources required Networks to add port mappings. If no resources 173 // are required, we assume the VM is a purely compute job and does not require 174 // the outside world to be able to reach it. VMs ran without port mappings can 175 // still reach out to the world, but without port mappings it is effectively 176 // firewalled 177 if len(task.Resources.Networks) > 0 { 178 // TODO: Consolidate these into map of host/guest port when we have HCL 179 // Note: Host port must be open and available 180 // Get and split guest ports. The guest_ports configuration must match up with 181 // the Reserved ports in the Task Resources 182 // Users can supply guest_hosts as a list of posts to map on the guest vm. 183 // These map 1:1 with the requested Reserved Ports from the hostmachine. 184 ports := strings.Split(task.Config["guest_ports"], ",") 185 if len(ports) == 0 { 186 return nil, fmt.Errorf("[ERR] driver.qemu: Error parsing required Guest Ports") 187 } 188 189 // TODO: support more than a single, default Network 190 if len(ports) != len(task.Resources.Networks[0].ReservedPorts) { 191 return nil, fmt.Errorf("[ERR] driver.qemu: Error matching Guest Ports with Reserved ports") 192 } 193 194 // Loop through the reserved ports and construct the hostfwd string, to map 195 // reserved ports to the ports listenting in the VM 196 // Ex: 197 // hostfwd=tcp::22000-:22,hostfwd=tcp::80-:8080 198 reservedPorts := task.Resources.Networks[0].ReservedPorts 199 var forwarding string 200 for i, p := range ports { 201 forwarding = fmt.Sprintf("%s,hostfwd=tcp::%s-:%s", forwarding, strconv.Itoa(reservedPorts[i]), p) 202 } 203 204 if "" == forwarding { 205 return nil, fmt.Errorf("[ERR] driver.qemu: Error constructing port forwarding") 206 } 207 208 args = append(args, 209 "-netdev", 210 fmt.Sprintf("user,id=user.0%s", forwarding), 211 "-device", "virtio-net,netdev=user.0", 212 ) 213 } 214 215 // If using KVM, add optimization args 216 if accelerator == "kvm" { 217 args = append(args, 218 "-enable-kvm", 219 "-cpu", "host", 220 // Do we have cores information available to the Driver? 221 // "-smp", fmt.Sprintf("%d", cores), 222 ) 223 } 224 225 // Start Qemu 226 var outBuf, errBuf bytes.Buffer 227 cmd := exec.Command(args[0], args[1:]...) 228 cmd.Stdout = &outBuf 229 cmd.Stderr = &errBuf 230 231 d.logger.Printf("[DEBUG] Starting QemuVM command: %q", strings.Join(args, " ")) 232 if err := cmd.Start(); err != nil { 233 return nil, fmt.Errorf( 234 "Error running QEMU: %s\n\nOutput: %s\n\nError: %s", 235 err, outBuf.String(), errBuf.String()) 236 } 237 238 d.logger.Printf("[INFO] Started new QemuVM: %s", vmID) 239 240 // Create and Return Handle 241 h := &qemuHandle{ 242 proc: cmd.Process, 243 vmID: vmPath.Name(), 244 doneCh: make(chan struct{}), 245 waitCh: make(chan error, 1), 246 } 247 248 go h.run() 249 return h, nil 250 } 251 252 func (d *QemuDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, error) { 253 // Parse the handle 254 pidBytes := []byte(strings.TrimPrefix(handleID, "QEMU:")) 255 qpid := &qemuPID{} 256 if err := json.Unmarshal(pidBytes, qpid); err != nil { 257 return nil, fmt.Errorf("failed to parse Qemu handle '%s': %v", handleID, err) 258 } 259 260 // Find the process 261 proc, err := os.FindProcess(qpid.Pid) 262 if proc == nil || err != nil { 263 return nil, fmt.Errorf("failed to find Qemu PID %d: %v", qpid.Pid, err) 264 } 265 266 // Return a driver handle 267 h := &qemuHandle{ 268 proc: proc, 269 vmID: qpid.VmID, 270 doneCh: make(chan struct{}), 271 waitCh: make(chan error, 1), 272 } 273 274 go h.run() 275 return h, nil 276 } 277 278 func (h *qemuHandle) ID() string { 279 // Return a handle to the PID 280 pid := &qemuPID{ 281 Pid: h.proc.Pid, 282 VmID: h.vmID, 283 } 284 data, err := json.Marshal(pid) 285 if err != nil { 286 log.Printf("[ERR] failed to marshal Qemu PID to JSON: %s", err) 287 } 288 return fmt.Sprintf("QEMU:%s", string(data)) 289 } 290 291 func (h *qemuHandle) WaitCh() chan error { 292 return h.waitCh 293 } 294 295 func (h *qemuHandle) Update(task *structs.Task) error { 296 // Update is not possible 297 return nil 298 } 299 300 // Kill is used to terminate the task. We send an Interrupt 301 // and then provide a 5 second grace period before doing a Kill. 302 // 303 // TODO: allow a 'shutdown_command' that can be executed over a ssh connection 304 // to the VM 305 func (h *qemuHandle) Kill() error { 306 h.proc.Signal(os.Interrupt) 307 select { 308 case <-h.doneCh: 309 return nil 310 case <-time.After(5 * time.Second): 311 return h.proc.Kill() 312 } 313 } 314 315 func (h *qemuHandle) run() { 316 ps, err := h.proc.Wait() 317 close(h.doneCh) 318 if err != nil { 319 h.waitCh <- err 320 } else if !ps.Success() { 321 h.waitCh <- fmt.Errorf("task exited with error") 322 } 323 close(h.waitCh) 324 }