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  }