github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/drivers/qemu/driver_test.go (about)

     1  package qemu
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/hashicorp/nomad/ci"
    12  	ctestutil "github.com/hashicorp/nomad/client/testutil"
    13  	"github.com/hashicorp/nomad/helper/pluginutils/hclutils"
    14  	"github.com/hashicorp/nomad/helper/testlog"
    15  	"github.com/hashicorp/nomad/helper/uuid"
    16  	"github.com/hashicorp/nomad/nomad/structs"
    17  	"github.com/hashicorp/nomad/plugins/drivers"
    18  	dtestutil "github.com/hashicorp/nomad/plugins/drivers/testutils"
    19  	"github.com/hashicorp/nomad/testutil"
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  // TODO(preetha) - tests remaining
    24  // using monitor socket for graceful shutdown
    25  
    26  // Verifies starting a qemu image and stopping it
    27  func TestQemuDriver_Start_Wait_Stop(t *testing.T) {
    28  	ci.Parallel(t)
    29  	ctestutil.QemuCompatible(t)
    30  
    31  	require := require.New(t)
    32  	ctx, cancel := context.WithCancel(context.Background())
    33  	defer cancel()
    34  
    35  	d := NewQemuDriver(ctx, testlog.HCLogger(t))
    36  	harness := dtestutil.NewDriverHarness(t, d)
    37  
    38  	task := &drivers.TaskConfig{
    39  		ID:   uuid.Generate(),
    40  		Name: "linux",
    41  		Resources: &drivers.Resources{
    42  			NomadResources: &structs.AllocatedTaskResources{
    43  				Memory: structs.AllocatedMemoryResources{
    44  					MemoryMB: 512,
    45  				},
    46  				Cpu: structs.AllocatedCpuResources{
    47  					CpuShares: 100,
    48  				},
    49  				Networks: []*structs.NetworkResource{
    50  					{
    51  						ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}},
    52  					},
    53  				},
    54  			},
    55  		},
    56  	}
    57  
    58  	tc := &TaskConfig{
    59  		ImagePath:        "linux-0.2.img",
    60  		Accelerator:      "tcg",
    61  		GracefulShutdown: false,
    62  		PortMap: map[string]int{
    63  			"main": 22,
    64  			"web":  8080,
    65  		},
    66  		Args: []string{"-nodefconfig", "-nodefaults"},
    67  	}
    68  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
    69  	cleanup := harness.MkAllocDir(task, true)
    70  	defer cleanup()
    71  
    72  	taskDir := filepath.Join(task.AllocDir, task.Name)
    73  
    74  	copyFile("./test-resources/linux-0.2.img", filepath.Join(taskDir, "linux-0.2.img"), t)
    75  
    76  	handle, _, err := harness.StartTask(task)
    77  	require.NoError(err)
    78  
    79  	require.NotNil(handle)
    80  
    81  	// Ensure that sending a Signal returns an error
    82  	err = d.SignalTask(task.ID, "SIGINT")
    83  	require.NotNil(err)
    84  
    85  	require.NoError(harness.DestroyTask(task.ID, true))
    86  
    87  }
    88  
    89  // copyFile moves an existing file to the destination
    90  func copyFile(src, dst string, t *testing.T) {
    91  	in, err := os.Open(src)
    92  	if err != nil {
    93  		t.Fatalf("copying %v -> %v failed: %v", src, dst, err)
    94  	}
    95  	defer in.Close()
    96  	out, err := os.Create(dst)
    97  	if err != nil {
    98  		t.Fatalf("copying %v -> %v failed: %v", src, dst, err)
    99  	}
   100  	defer func() {
   101  		if err := out.Close(); err != nil {
   102  			t.Fatalf("copying %v -> %v failed: %v", src, dst, err)
   103  		}
   104  	}()
   105  	if _, err = io.Copy(out, in); err != nil {
   106  		t.Fatalf("copying %v -> %v failed: %v", src, dst, err)
   107  	}
   108  	if err := out.Sync(); err != nil {
   109  		t.Fatalf("copying %v -> %v failed: %v", src, dst, err)
   110  	}
   111  }
   112  
   113  // Verifies starting a qemu image and stopping it
   114  func TestQemuDriver_User(t *testing.T) {
   115  	ci.Parallel(t)
   116  	ctestutil.QemuCompatible(t)
   117  
   118  	require := require.New(t)
   119  	ctx, cancel := context.WithCancel(context.Background())
   120  	defer cancel()
   121  
   122  	d := NewQemuDriver(ctx, testlog.HCLogger(t))
   123  	harness := dtestutil.NewDriverHarness(t, d)
   124  
   125  	task := &drivers.TaskConfig{
   126  		ID:   uuid.Generate(),
   127  		Name: "linux",
   128  		User: "alice",
   129  		Resources: &drivers.Resources{
   130  			NomadResources: &structs.AllocatedTaskResources{
   131  				Memory: structs.AllocatedMemoryResources{
   132  					MemoryMB: 512,
   133  				},
   134  				Cpu: structs.AllocatedCpuResources{
   135  					CpuShares: 100,
   136  				},
   137  				Networks: []*structs.NetworkResource{
   138  					{
   139  						ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}},
   140  					},
   141  				},
   142  			},
   143  		},
   144  	}
   145  
   146  	tc := &TaskConfig{
   147  		ImagePath:        "linux-0.2.img",
   148  		Accelerator:      "tcg",
   149  		GracefulShutdown: false,
   150  		PortMap: map[string]int{
   151  			"main": 22,
   152  			"web":  8080,
   153  		},
   154  		Args: []string{"-nodefconfig", "-nodefaults"},
   155  	}
   156  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   157  	cleanup := harness.MkAllocDir(task, true)
   158  	defer cleanup()
   159  
   160  	taskDir := filepath.Join(task.AllocDir, task.Name)
   161  
   162  	copyFile("./test-resources/linux-0.2.img", filepath.Join(taskDir, "linux-0.2.img"), t)
   163  
   164  	_, _, err := harness.StartTask(task)
   165  	require.Error(err)
   166  	require.Contains(err.Error(), "unknown user alice", err.Error())
   167  
   168  }
   169  
   170  //	Verifies getting resource usage stats
   171  //
   172  // TODO(preetha) this test needs random sleeps to pass
   173  func TestQemuDriver_Stats(t *testing.T) {
   174  	ci.Parallel(t)
   175  	ctestutil.QemuCompatible(t)
   176  
   177  	require := require.New(t)
   178  	ctx, cancel := context.WithCancel(context.Background())
   179  	defer cancel()
   180  
   181  	d := NewQemuDriver(ctx, testlog.HCLogger(t))
   182  	harness := dtestutil.NewDriverHarness(t, d)
   183  
   184  	task := &drivers.TaskConfig{
   185  		ID:   uuid.Generate(),
   186  		Name: "linux",
   187  		Resources: &drivers.Resources{
   188  			NomadResources: &structs.AllocatedTaskResources{
   189  				Memory: structs.AllocatedMemoryResources{
   190  					MemoryMB: 512,
   191  				},
   192  				Cpu: structs.AllocatedCpuResources{
   193  					CpuShares: 100,
   194  				},
   195  				Networks: []*structs.NetworkResource{
   196  					{
   197  						ReservedPorts: []structs.Port{{Label: "main", Value: 22000}, {Label: "web", Value: 80}},
   198  					},
   199  				},
   200  			},
   201  		},
   202  	}
   203  
   204  	tc := &TaskConfig{
   205  		ImagePath:        "linux-0.2.img",
   206  		Accelerator:      "tcg",
   207  		GracefulShutdown: false,
   208  		PortMap: map[string]int{
   209  			"main": 22,
   210  			"web":  8080,
   211  		},
   212  		Args: []string{"-nodefconfig", "-nodefaults"},
   213  	}
   214  	require.NoError(task.EncodeConcreteDriverConfig(&tc))
   215  	cleanup := harness.MkAllocDir(task, true)
   216  	defer cleanup()
   217  
   218  	taskDir := filepath.Join(task.AllocDir, task.Name)
   219  
   220  	copyFile("./test-resources/linux-0.2.img", filepath.Join(taskDir, "linux-0.2.img"), t)
   221  
   222  	handle, _, err := harness.StartTask(task)
   223  	require.NoError(err)
   224  
   225  	require.NotNil(handle)
   226  
   227  	// Wait for task to start
   228  	_, err = harness.WaitTask(context.Background(), handle.Config.ID)
   229  	require.NoError(err)
   230  
   231  	// Wait until task started
   232  	require.NoError(harness.WaitUntilStarted(task.ID, 1*time.Second))
   233  	time.Sleep(30 * time.Second)
   234  	statsCtx, cancel := context.WithCancel(context.Background())
   235  	defer cancel()
   236  	statsCh, err := harness.TaskStats(statsCtx, task.ID, time.Second*10)
   237  	require.NoError(err)
   238  
   239  	select {
   240  	case stats := <-statsCh:
   241  		t.Logf("CPU:%+v Memory:%+v\n", stats.ResourceUsage.CpuStats, stats.ResourceUsage.MemoryStats)
   242  		require.NotZero(stats.ResourceUsage.MemoryStats.RSS)
   243  		require.NoError(harness.DestroyTask(task.ID, true))
   244  	case <-time.After(time.Second * 1):
   245  		require.Fail("timeout receiving from stats")
   246  	}
   247  
   248  }
   249  
   250  func TestQemuDriver_Fingerprint(t *testing.T) {
   251  	ci.Parallel(t)
   252  	require := require.New(t)
   253  
   254  	ctestutil.QemuCompatible(t)
   255  
   256  	ctx, cancel := context.WithCancel(context.Background())
   257  	defer cancel()
   258  
   259  	d := NewQemuDriver(ctx, testlog.HCLogger(t))
   260  	harness := dtestutil.NewDriverHarness(t, d)
   261  
   262  	fingerCh, err := harness.Fingerprint(context.Background())
   263  	require.NoError(err)
   264  	select {
   265  	case finger := <-fingerCh:
   266  		require.Equal(drivers.HealthStateHealthy, finger.Health)
   267  		require.True(finger.Attributes["driver.qemu"].GetBool())
   268  	case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second):
   269  		require.Fail("timeout receiving fingerprint")
   270  	}
   271  }
   272  
   273  func TestConfig_ParseAllHCL(t *testing.T) {
   274  	ci.Parallel(t)
   275  
   276  	cfgStr := `
   277  config {
   278    image_path = "/tmp/image_path"
   279    drive_interface = "virtio"
   280    accelerator = "kvm"
   281    args = ["arg1", "arg2"]
   282    port_map {
   283      http = 80
   284      https = 443
   285    }
   286    graceful_shutdown = true
   287  }`
   288  
   289  	expected := &TaskConfig{
   290  		ImagePath:      "/tmp/image_path",
   291  		DriveInterface: "virtio",
   292  		Accelerator:    "kvm",
   293  		Args:           []string{"arg1", "arg2"},
   294  		PortMap: map[string]int{
   295  			"http":  80,
   296  			"https": 443,
   297  		},
   298  		GracefulShutdown: true,
   299  	}
   300  
   301  	var tc *TaskConfig
   302  	hclutils.NewConfigParser(taskConfigSpec).ParseHCL(t, cfgStr, &tc)
   303  
   304  	require.EqualValues(t, expected, tc)
   305  }
   306  
   307  func TestIsAllowedDriveInterface(t *testing.T) {
   308  	validInterfaces := []string{"ide", "scsi", "sd", "mtd", "floppy", "pflash", "virtio", "none"}
   309  	invalidInterfaces := []string{"foo", "virtio-foo"}
   310  
   311  	for _, i := range validInterfaces {
   312  		require.Truef(t, isAllowedDriveInterface(i), "drive_interface should be allowed: %v", i)
   313  	}
   314  
   315  	for _, i := range invalidInterfaces {
   316  		require.Falsef(t, isAllowedDriveInterface(i), "drive_interface should be not allowed: %v", i)
   317  	}
   318  }
   319  
   320  func TestIsAllowedImagePath(t *testing.T) {
   321  	ci.Parallel(t)
   322  
   323  	allowedPaths := []string{"/tmp", "/opt/qemu"}
   324  	allocDir := "/opt/nomad/some-alloc-dir"
   325  
   326  	validPaths := []string{
   327  		"local/path",
   328  		"/tmp/subdir/qemu-image",
   329  		"/opt/qemu/image",
   330  		"/opt/qemu/subdir/image",
   331  		"/opt/nomad/some-alloc-dir/local/image.img",
   332  	}
   333  
   334  	invalidPaths := []string{
   335  		"/image.img",
   336  		"../image.img",
   337  		"/tmpimage.img",
   338  		"/opt/other/image.img",
   339  		"/opt/nomad-submatch.img",
   340  	}
   341  
   342  	for _, p := range validPaths {
   343  		require.Truef(t, isAllowedImagePath(allowedPaths, allocDir, p), "path should be allowed: %v", p)
   344  	}
   345  
   346  	for _, p := range invalidPaths {
   347  		require.Falsef(t, isAllowedImagePath(allowedPaths, allocDir, p), "path should be not allowed: %v", p)
   348  	}
   349  }
   350  
   351  func TestArgsAllowList(t *testing.T) {
   352  	ci.Parallel(t)
   353  
   354  	pluginConfigAllowList := []string{"-drive", "-net", "-snapshot"}
   355  
   356  	validArgs := [][]string{
   357  		{"-drive", "/path/to/wherever", "-snapshot"},
   358  		{"-net", "tap,vlan=0,ifname=tap0"},
   359  	}
   360  
   361  	invalidArgs := [][]string{
   362  		{"-usbdevice", "mouse"},
   363  		{"-singlestep"},
   364  		{"--singlestep"},
   365  		{" -singlestep"},
   366  		{"\t-singlestep"},
   367  	}
   368  
   369  	for _, args := range validArgs {
   370  		require.NoError(t, validateArgs(pluginConfigAllowList, args))
   371  		require.NoError(t, validateArgs([]string{}, args))
   372  
   373  	}
   374  	for _, args := range invalidArgs {
   375  		require.Error(t, validateArgs(pluginConfigAllowList, args))
   376  		require.NoError(t, validateArgs([]string{}, args))
   377  	}
   378  
   379  }