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  }