github.com/ryanslade/nomad@v0.2.4-0.20160128061903-fc95782f2089/client/driver/qemu.go (about)

     1  package driver
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"log"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"regexp"
    10  	"runtime"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/hashicorp/nomad/client/allocdir"
    15  	"github.com/hashicorp/nomad/client/config"
    16  	"github.com/hashicorp/nomad/client/driver/executor"
    17  	cstructs "github.com/hashicorp/nomad/client/driver/structs"
    18  	"github.com/hashicorp/nomad/client/fingerprint"
    19  	"github.com/hashicorp/nomad/client/getter"
    20  	"github.com/hashicorp/nomad/nomad/structs"
    21  	"github.com/mitchellh/mapstructure"
    22  )
    23  
    24  var (
    25  	reQemuVersion = regexp.MustCompile(`version (\d[\.\d+]+)`)
    26  )
    27  
    28  // QemuDriver is a driver for running images via Qemu
    29  // We attempt to chose sane defaults for now, with more configuration available
    30  // planned in the future
    31  type QemuDriver struct {
    32  	DriverContext
    33  	fingerprint.StaticFingerprinter
    34  }
    35  
    36  type QemuDriverConfig struct {
    37  	ArtifactSource string           `mapstructure:"artifact_source"`
    38  	Checksum       string           `mapstructure:"checksum"`
    39  	Accelerator    string           `mapstructure:"accelerator"`
    40  	PortMap        []map[string]int `mapstructure:"port_map"` // A map of host port labels and to guest ports.
    41  }
    42  
    43  // qemuHandle is returned from Start/Open as a handle to the PID
    44  type qemuHandle struct {
    45  	cmd         executor.Executor
    46  	killTimeout time.Duration
    47  	logger      *log.Logger
    48  	waitCh      chan *cstructs.WaitResult
    49  	doneCh      chan struct{}
    50  }
    51  
    52  // NewQemuDriver is used to create a new exec driver
    53  func NewQemuDriver(ctx *DriverContext) Driver {
    54  	return &QemuDriver{DriverContext: *ctx}
    55  }
    56  
    57  func (d *QemuDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) {
    58  	bin := "qemu-system-x86_64"
    59  	if runtime.GOOS == "windows" {
    60  		// On windows, the "qemu-system-x86_64" command does not respond to the
    61  		// version flag.
    62  		bin = "qemu-img"
    63  	}
    64  	outBytes, err := exec.Command(bin, "--version").Output()
    65  	if err != nil {
    66  		return false, nil
    67  	}
    68  	out := strings.TrimSpace(string(outBytes))
    69  
    70  	matches := reQemuVersion.FindStringSubmatch(out)
    71  	if len(matches) != 2 {
    72  		return false, fmt.Errorf("Unable to parse Qemu version string: %#v", matches)
    73  	}
    74  
    75  	node.Attributes["driver.qemu"] = "1"
    76  	node.Attributes["driver.qemu.version"] = matches[1]
    77  
    78  	return true, nil
    79  }
    80  
    81  // Run an existing Qemu image. Start() will pull down an existing, valid Qemu
    82  // image and save it to the Drivers Allocation Dir
    83  func (d *QemuDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) {
    84  	var driverConfig QemuDriverConfig
    85  	if err := mapstructure.WeakDecode(task.Config, &driverConfig); err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	if len(driverConfig.PortMap) > 1 {
    90  		return nil, fmt.Errorf("Only one port_map block is allowed in the qemu driver config")
    91  	}
    92  
    93  	// Get the image source
    94  	source, ok := task.Config["artifact_source"]
    95  	if !ok || source == "" {
    96  		return nil, fmt.Errorf("Missing source image Qemu driver")
    97  	}
    98  
    99  	// Qemu defaults to 128M of RAM for a given VM. Instead, we force users to
   100  	// supply a memory size in the tasks resources
   101  	if task.Resources == nil || task.Resources.MemoryMB == 0 {
   102  		return nil, fmt.Errorf("Missing required Task Resource: Memory")
   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  
   111  	// Proceed to download an artifact to be executed.
   112  	vmPath, err := getter.GetArtifact(
   113  		filepath.Join(taskDir, allocdir.TaskLocal),
   114  		driverConfig.ArtifactSource,
   115  		driverConfig.Checksum,
   116  		d.logger,
   117  	)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	vmID := filepath.Base(vmPath)
   123  
   124  	// Parse configuration arguments
   125  	// Create the base arguments
   126  	accelerator := "tcg"
   127  	if driverConfig.Accelerator != "" {
   128  		accelerator = driverConfig.Accelerator
   129  	}
   130  	// TODO: Check a lower bounds, e.g. the default 128 of Qemu
   131  	mem := fmt.Sprintf("%dM", task.Resources.MemoryMB)
   132  
   133  	args := []string{
   134  		"qemu-system-x86_64",
   135  		"-machine", "type=pc,accel=" + accelerator,
   136  		"-name", vmID,
   137  		"-m", mem,
   138  		"-drive", "file=" + vmPath,
   139  		"-nodefconfig",
   140  		"-nodefaults",
   141  		"-nographic",
   142  	}
   143  
   144  	// Check the Resources required Networks to add port mappings. If no resources
   145  	// are required, we assume the VM is a purely compute job and does not require
   146  	// the outside world to be able to reach it. VMs ran without port mappings can
   147  	// still reach out to the world, but without port mappings it is effectively
   148  	// firewalled
   149  	protocols := []string{"udp", "tcp"}
   150  	if len(task.Resources.Networks) > 0 && len(driverConfig.PortMap) == 1 {
   151  		// Loop through the port map and construct the hostfwd string, to map
   152  		// reserved ports to the ports listenting in the VM
   153  		// Ex: hostfwd=tcp::22000-:22,hostfwd=tcp::80-:8080
   154  		var forwarding []string
   155  		taskPorts := task.Resources.Networks[0].MapLabelToValues(nil)
   156  		for label, guest := range driverConfig.PortMap[0] {
   157  			host, ok := taskPorts[label]
   158  			if !ok {
   159  				return nil, fmt.Errorf("Unknown port label %q", label)
   160  			}
   161  
   162  			for _, p := range protocols {
   163  				forwarding = append(forwarding, fmt.Sprintf("hostfwd=%s::%d-:%d", p, host, guest))
   164  			}
   165  		}
   166  
   167  		if len(forwarding) != 0 {
   168  			args = append(args,
   169  				"-netdev",
   170  				fmt.Sprintf("user,id=user.0,%s", strings.Join(forwarding, ",")),
   171  				"-device", "virtio-net,netdev=user.0",
   172  			)
   173  		}
   174  	}
   175  
   176  	// If using KVM, add optimization args
   177  	if accelerator == "kvm" {
   178  		args = append(args,
   179  			"-enable-kvm",
   180  			"-cpu", "host",
   181  			// Do we have cores information available to the Driver?
   182  			// "-smp", fmt.Sprintf("%d", cores),
   183  		)
   184  	}
   185  
   186  	// Setup the command
   187  	execCtx := executor.NewExecutorContext(d.taskEnv)
   188  	cmd := executor.Command(execCtx, args[0], args[1:]...)
   189  	if err := cmd.Limit(task.Resources); err != nil {
   190  		return nil, fmt.Errorf("failed to constrain resources: %s", err)
   191  	}
   192  
   193  	if err := cmd.ConfigureTaskDir(d.taskName, ctx.AllocDir); err != nil {
   194  		return nil, fmt.Errorf("failed to configure task directory: %v", err)
   195  	}
   196  
   197  	d.logger.Printf("[DEBUG] Starting QemuVM command: %q", strings.Join(args, " "))
   198  	if err := cmd.Start(); err != nil {
   199  		return nil, fmt.Errorf("failed to start command: %v", err)
   200  	}
   201  	d.logger.Printf("[INFO] Started new QemuVM: %s", vmID)
   202  
   203  	// Create and Return Handle
   204  	h := &qemuHandle{
   205  		cmd:         cmd,
   206  		killTimeout: d.DriverContext.KillTimeout(task),
   207  		logger:      d.logger,
   208  		doneCh:      make(chan struct{}),
   209  		waitCh:      make(chan *cstructs.WaitResult, 1),
   210  	}
   211  
   212  	go h.run()
   213  	return h, nil
   214  }
   215  
   216  type qemuId struct {
   217  	ExecutorId  string
   218  	KillTimeout time.Duration
   219  }
   220  
   221  func (d *QemuDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, error) {
   222  	id := &qemuId{}
   223  	if err := json.Unmarshal([]byte(handleID), id); err != nil {
   224  		return nil, fmt.Errorf("Failed to parse handle '%s': %v", handleID, err)
   225  	}
   226  
   227  	// Find the process
   228  	execCtx := executor.NewExecutorContext(d.taskEnv)
   229  	cmd, err := executor.OpenId(execCtx, id.ExecutorId)
   230  	if err != nil {
   231  		return nil, fmt.Errorf("failed to open ID %v: %v", id.ExecutorId, err)
   232  	}
   233  
   234  	// Return a driver handle
   235  	h := &execHandle{
   236  		cmd:         cmd,
   237  		logger:      d.logger,
   238  		killTimeout: id.KillTimeout,
   239  		doneCh:      make(chan struct{}),
   240  		waitCh:      make(chan *cstructs.WaitResult, 1),
   241  	}
   242  	go h.run()
   243  	return h, nil
   244  }
   245  
   246  func (h *qemuHandle) ID() string {
   247  	executorId, _ := h.cmd.ID()
   248  	id := qemuId{
   249  		ExecutorId:  executorId,
   250  		KillTimeout: h.killTimeout,
   251  	}
   252  
   253  	data, err := json.Marshal(id)
   254  	if err != nil {
   255  		h.logger.Printf("[ERR] driver.qemu: failed to marshal ID to JSON: %s", err)
   256  	}
   257  	return string(data)
   258  }
   259  
   260  func (h *qemuHandle) WaitCh() chan *cstructs.WaitResult {
   261  	return h.waitCh
   262  }
   263  
   264  func (h *qemuHandle) Update(task *structs.Task) error {
   265  	// Update is not possible
   266  	return nil
   267  }
   268  
   269  // TODO: allow a 'shutdown_command' that can be executed over a ssh connection
   270  // to the VM
   271  func (h *qemuHandle) Kill() error {
   272  	h.cmd.Shutdown()
   273  	select {
   274  	case <-h.doneCh:
   275  		return nil
   276  	case <-time.After(h.killTimeout):
   277  		return h.cmd.ForceStop()
   278  	}
   279  }
   280  
   281  func (h *qemuHandle) run() {
   282  	res := h.cmd.Wait()
   283  	close(h.doneCh)
   284  	h.waitCh <- res
   285  	close(h.waitCh)
   286  }