github.com/hernad/nomad@v1.6.112/drivers/qemu/driver_test.go (about)

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