...

Source file src/github.com/opencontainers/runc/libcontainer/cgroups/systemd/systemd_test.go

Documentation: github.com/opencontainers/runc/libcontainer/cgroups/systemd

     1  package systemd
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"os"
     7  	"os/exec"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/opencontainers/runc/libcontainer/cgroups"
    12  	"github.com/opencontainers/runc/libcontainer/configs"
    13  	"github.com/opencontainers/runc/libcontainer/devices"
    14  )
    15  
    16  func newManager(t *testing.T, config *configs.Cgroup) (m cgroups.Manager) {
    17  	t.Helper()
    18  	var err error
    19  
    20  	if cgroups.IsCgroup2UnifiedMode() {
    21  		m, err = NewUnifiedManager(config, "")
    22  	} else {
    23  		m, err = NewLegacyManager(config, nil)
    24  	}
    25  	if err != nil {
    26  		t.Fatal(err)
    27  	}
    28  	t.Cleanup(func() { _ = m.Destroy() })
    29  
    30  	return m
    31  }
    32  
    33  func TestSystemdVersion(t *testing.T) {
    34  	systemdVersionTests := []struct {
    35  		verStr      string
    36  		expectedVer int
    37  		expectErr   bool
    38  	}{
    39  		{`"219"`, 219, false},
    40  		{`"v245.4-1.fc32"`, 245, false},
    41  		{`"241-1"`, 241, false},
    42  		{`"v241-1"`, 241, false},
    43  		{"NaN", 0, true},
    44  		{"", 0, true},
    45  	}
    46  	for _, sdTest := range systemdVersionTests {
    47  		ver, err := systemdVersionAtoi(sdTest.verStr)
    48  		if !sdTest.expectErr && err != nil {
    49  			t.Errorf("systemdVersionAtoi(%s); want nil; got %v", sdTest.verStr, err)
    50  		}
    51  		if sdTest.expectErr && err == nil {
    52  			t.Errorf("systemdVersionAtoi(%s); wanted failure; got nil", sdTest.verStr)
    53  		}
    54  		if ver != sdTest.expectedVer {
    55  			t.Errorf("systemdVersionAtoi(%s); want %d; got %d", sdTest.verStr, sdTest.expectedVer, ver)
    56  		}
    57  	}
    58  }
    59  
    60  func TestValidUnitTypes(t *testing.T) {
    61  	testCases := []struct {
    62  		unitName         string
    63  		expectedUnitType string
    64  	}{
    65  		{"system.slice", "Slice"},
    66  		{"kubepods.slice", "Slice"},
    67  		{"testing-container:ab.scope", "Scope"},
    68  	}
    69  	for _, sdTest := range testCases {
    70  		unitType := getUnitType(sdTest.unitName)
    71  		if unitType != sdTest.expectedUnitType {
    72  			t.Errorf("getUnitType(%s); want %q; got %q", sdTest.unitName, sdTest.expectedUnitType, unitType)
    73  		}
    74  	}
    75  }
    76  
    77  // TestPodSkipDevicesUpdate checks that updating a pod having SkipDevices: true
    78  // does not result in spurious "permission denied" errors in a container
    79  // running under the pod. The test is somewhat similar in nature to the
    80  // @test "update devices [minimal transition rules]" in tests/integration,
    81  // but uses a pod.
    82  func TestPodSkipDevicesUpdate(t *testing.T) {
    83  	if !IsRunningSystemd() {
    84  		t.Skip("Test requires systemd.")
    85  	}
    86  	if os.Geteuid() != 0 {
    87  		t.Skip("Test requires root.")
    88  	}
    89  
    90  	podName := "system-runc_test_pod" + t.Name() + ".slice"
    91  	podConfig := &configs.Cgroup{
    92  		Systemd: true,
    93  		Parent:  "system.slice",
    94  		Name:    podName,
    95  		Resources: &configs.Resources{
    96  			PidsLimit:   42,
    97  			Memory:      32 * 1024 * 1024,
    98  			SkipDevices: true,
    99  		},
   100  	}
   101  	// Create "pod" cgroup (a systemd slice to hold containers).
   102  	pm := newManager(t, podConfig)
   103  	if err := pm.Apply(-1); err != nil {
   104  		t.Fatal(err)
   105  	}
   106  	if err := pm.Set(podConfig.Resources); err != nil {
   107  		t.Fatal(err)
   108  	}
   109  
   110  	containerConfig := &configs.Cgroup{
   111  		Parent:      podName,
   112  		ScopePrefix: "test",
   113  		Name:        "PodSkipDevicesUpdate",
   114  		Resources: &configs.Resources{
   115  			Devices: []*devices.Rule{
   116  				// Allow access to /dev/null.
   117  				{
   118  					Type:        devices.CharDevice,
   119  					Major:       1,
   120  					Minor:       3,
   121  					Permissions: "rwm",
   122  					Allow:       true,
   123  				},
   124  			},
   125  		},
   126  	}
   127  
   128  	// Create a "container" within the "pod" cgroup.
   129  	// This is not a real container, just a process in the cgroup.
   130  	cmd := exec.Command("sleep", "infinity")
   131  	cmd.Env = append(os.Environ(), "LANG=C")
   132  	var stderr bytes.Buffer
   133  	cmd.Stderr = &stderr
   134  	if err := cmd.Start(); err != nil {
   135  		t.Fatal(err)
   136  	}
   137  	// Make sure to not leave a zombie.
   138  	defer func() {
   139  		// These may fail, we don't care.
   140  		_ = cmd.Process.Kill()
   141  		_ = cmd.Wait()
   142  	}()
   143  
   144  	// Put the process into a cgroup.
   145  	cm := newManager(t, containerConfig)
   146  	if err := cm.Apply(cmd.Process.Pid); err != nil {
   147  		t.Fatal(err)
   148  	}
   149  	// Check that we put the "container" into the "pod" cgroup.
   150  	if !strings.HasPrefix(cm.Path("devices"), pm.Path("devices")) {
   151  		t.Fatalf("expected container cgroup path %q to be under pod cgroup path %q",
   152  			cm.Path("devices"), pm.Path("devices"))
   153  	}
   154  	if err := cm.Set(containerConfig.Resources); err != nil {
   155  		t.Fatal(err)
   156  	}
   157  
   158  	// Now update the pod a few times.
   159  	for i := 0; i < 42; i++ {
   160  		podConfig.Resources.PidsLimit++
   161  		podConfig.Resources.Memory += 1024 * 1024
   162  		if err := pm.Set(podConfig.Resources); err != nil {
   163  			t.Fatal(err)
   164  		}
   165  	}
   166  	// Kill the "container".
   167  	if err := cmd.Process.Kill(); err != nil {
   168  		t.Fatal(err)
   169  	}
   170  
   171  	_ = cmd.Wait()
   172  
   173  	// "Container" stderr should be empty.
   174  	if stderr.Len() != 0 {
   175  		t.Fatalf("container stderr not empty: %s", stderr.String())
   176  	}
   177  }
   178  
   179  func testSkipDevices(t *testing.T, skipDevices bool, expected []string) {
   180  	if !IsRunningSystemd() {
   181  		t.Skip("Test requires systemd.")
   182  	}
   183  	if os.Geteuid() != 0 {
   184  		t.Skip("Test requires root.")
   185  	}
   186  	// https://github.com/opencontainers/runc/issues/3743
   187  	centosVer, _ := exec.Command("rpm", "-q", "--qf", "%{version}", "centos-release").CombinedOutput()
   188  	if string(centosVer) == "7" {
   189  		t.Skip("Flaky on CentOS 7")
   190  	}
   191  
   192  	podConfig := &configs.Cgroup{
   193  		Parent: "system.slice",
   194  		Name:   "system-runc_test_pods.slice",
   195  		Resources: &configs.Resources{
   196  			SkipDevices: skipDevices,
   197  		},
   198  	}
   199  	// Create "pods" cgroup (a systemd slice to hold containers).
   200  	pm := newManager(t, podConfig)
   201  	if err := pm.Apply(-1); err != nil {
   202  		t.Fatal(err)
   203  	}
   204  	if err := pm.Set(podConfig.Resources); err != nil {
   205  		t.Fatal(err)
   206  	}
   207  
   208  	config := &configs.Cgroup{
   209  		Parent:      "system-runc_test_pods.slice",
   210  		ScopePrefix: "test",
   211  		Name:        "SkipDevices",
   212  		Resources: &configs.Resources{
   213  			Devices: []*devices.Rule{
   214  				// Allow access to /dev/full only.
   215  				{
   216  					Type:        devices.CharDevice,
   217  					Major:       1,
   218  					Minor:       7,
   219  					Permissions: "rwm",
   220  					Allow:       true,
   221  				},
   222  			},
   223  		},
   224  	}
   225  
   226  	// Create a "container" within the "pods" cgroup.
   227  	// This is not a real container, just a process in the cgroup.
   228  	cmd := exec.Command("bash", "-c", "read; echo > /dev/full; cat /dev/null; true")
   229  	cmd.Env = append(os.Environ(), "LANG=C")
   230  	stdinR, stdinW, err := os.Pipe()
   231  	if err != nil {
   232  		t.Fatal(err)
   233  	}
   234  	cmd.Stdin = stdinR
   235  	var stderr bytes.Buffer
   236  	cmd.Stderr = &stderr
   237  	err = cmd.Start()
   238  	stdinR.Close()
   239  	defer stdinW.Close()
   240  	if err != nil {
   241  		t.Fatal(err)
   242  	}
   243  	// Make sure to not leave a zombie.
   244  	defer func() {
   245  		// These may fail, we don't care.
   246  		_, _ = stdinW.WriteString("hey\n")
   247  		_ = cmd.Wait()
   248  	}()
   249  
   250  	// Put the process into a cgroup.
   251  	m := newManager(t, config)
   252  	if err := m.Apply(cmd.Process.Pid); err != nil {
   253  		t.Fatal(err)
   254  	}
   255  	// Check that we put the "container" into the "pod" cgroup.
   256  	if !strings.HasPrefix(m.Path("devices"), pm.Path("devices")) {
   257  		t.Fatalf("expected container cgroup path %q to be under pod cgroup path %q",
   258  			m.Path("devices"), pm.Path("devices"))
   259  	}
   260  	if err := m.Set(config.Resources); err != nil {
   261  		// failed to write "c 1:7 rwm": write /sys/fs/cgroup/devices/system.slice/system-runc_test_pods.slice/test-SkipDevices.scope/devices.allow: operation not permitted
   262  		if skipDevices == false && strings.HasSuffix(err.Error(), "/devices.allow: operation not permitted") {
   263  			// Cgroup v1 devices controller gives EPERM on trying
   264  			// to enable devices that are not enabled
   265  			// (skipDevices=false) in a parent cgroup.
   266  			// If this happens, test is passing.
   267  			return
   268  		}
   269  		t.Fatal(err)
   270  	}
   271  
   272  	// Check that we can access /dev/full but not /dev/zero.
   273  	if _, err := stdinW.WriteString("wow\n"); err != nil {
   274  		t.Fatal(err)
   275  	}
   276  	if err := cmd.Wait(); err != nil {
   277  		t.Fatal(err)
   278  	}
   279  	for _, exp := range expected {
   280  		if !strings.Contains(stderr.String(), exp) {
   281  			t.Errorf("expected %q, got: %s", exp, stderr.String())
   282  		}
   283  	}
   284  }
   285  
   286  func TestSkipDevicesTrue(t *testing.T) {
   287  	testSkipDevices(t, true, []string{
   288  		"echo: write error: No space left on device",
   289  		"cat: /dev/null: Operation not permitted",
   290  	})
   291  }
   292  
   293  func TestSkipDevicesFalse(t *testing.T) {
   294  	// If SkipDevices is not set for the parent slice, access to both
   295  	// devices should fail. This is done to assess the test correctness.
   296  	// For cgroup v1, we check for m.Set returning EPERM.
   297  	// For cgroup v2, we check for the errors below.
   298  	testSkipDevices(t, false, []string{
   299  		"/dev/full: Operation not permitted",
   300  		"cat: /dev/null: Operation not permitted",
   301  	})
   302  }
   303  
   304  func TestUnitExistsIgnored(t *testing.T) {
   305  	if !IsRunningSystemd() {
   306  		t.Skip("Test requires systemd.")
   307  	}
   308  	if os.Geteuid() != 0 {
   309  		t.Skip("Test requires root.")
   310  	}
   311  
   312  	podConfig := &configs.Cgroup{
   313  		Parent:    "system.slice",
   314  		Name:      "system-runc_test_exists.slice",
   315  		Resources: &configs.Resources{},
   316  	}
   317  	// Create "pods" cgroup (a systemd slice to hold containers).
   318  	pm := newManager(t, podConfig)
   319  
   320  	// create twice to make sure "UnitExists" error is ignored.
   321  	for i := 0; i < 2; i++ {
   322  		if err := pm.Apply(-1); err != nil {
   323  			t.Fatal(err)
   324  		}
   325  	}
   326  }
   327  
   328  func TestFreezePodCgroup(t *testing.T) {
   329  	if !IsRunningSystemd() {
   330  		t.Skip("Test requires systemd.")
   331  	}
   332  	if os.Geteuid() != 0 {
   333  		t.Skip("Test requires root.")
   334  	}
   335  
   336  	podConfig := &configs.Cgroup{
   337  		Parent: "system.slice",
   338  		Name:   "system-runc_test_pod.slice",
   339  		Resources: &configs.Resources{
   340  			SkipDevices: true,
   341  			Freezer:     configs.Frozen,
   342  		},
   343  	}
   344  	// Create a "pod" cgroup (a systemd slice to hold containers),
   345  	// which is frozen initially.
   346  	pm := newManager(t, podConfig)
   347  	if err := pm.Apply(-1); err != nil {
   348  		t.Fatal(err)
   349  	}
   350  
   351  	if err := pm.Set(podConfig.Resources); err != nil {
   352  		t.Fatal(err)
   353  	}
   354  
   355  	// Check the pod is frozen.
   356  	pf, err := pm.GetFreezerState()
   357  	if err != nil {
   358  		t.Fatal(err)
   359  	}
   360  	if pf != configs.Frozen {
   361  		t.Fatalf("expected pod to be frozen, got %v", pf)
   362  	}
   363  
   364  	// Create a "container" within the "pod" cgroup.
   365  	// This is not a real container, just a process in the cgroup.
   366  	containerConfig := &configs.Cgroup{
   367  		Parent:      "system-runc_test_pod.slice",
   368  		ScopePrefix: "test",
   369  		Name:        "inner-container",
   370  		Resources:   &configs.Resources{},
   371  	}
   372  
   373  	cmd := exec.Command("bash", "-c", "while read; do echo $REPLY; done")
   374  	cmd.Env = append(os.Environ(), "LANG=C")
   375  
   376  	// Setup stdin.
   377  	stdinR, stdinW, err := os.Pipe()
   378  	if err != nil {
   379  		t.Fatal(err)
   380  	}
   381  	cmd.Stdin = stdinR
   382  
   383  	// Setup stdout.
   384  	stdoutR, stdoutW, err := os.Pipe()
   385  	if err != nil {
   386  		t.Fatal(err)
   387  	}
   388  	cmd.Stdout = stdoutW
   389  	rdr := bufio.NewReader(stdoutR)
   390  
   391  	// Setup stderr.
   392  	var stderr bytes.Buffer
   393  	cmd.Stderr = &stderr
   394  
   395  	err = cmd.Start()
   396  	stdinR.Close()
   397  	stdoutW.Close()
   398  	defer func() {
   399  		_ = stdinW.Close()
   400  		_ = stdoutR.Close()
   401  	}()
   402  	if err != nil {
   403  		t.Fatal(err)
   404  	}
   405  	// Make sure to not leave a zombie.
   406  	defer func() {
   407  		// These may fail, we don't care.
   408  		_ = cmd.Process.Kill()
   409  		_ = cmd.Wait()
   410  	}()
   411  
   412  	// Put the process into a cgroup.
   413  	cm := newManager(t, containerConfig)
   414  
   415  	if err := cm.Apply(cmd.Process.Pid); err != nil {
   416  		t.Fatal(err)
   417  	}
   418  	if err := cm.Set(containerConfig.Resources); err != nil {
   419  		t.Fatal(err)
   420  	}
   421  	// Check that we put the "container" into the "pod" cgroup.
   422  	if !strings.HasPrefix(cm.Path("freezer"), pm.Path("freezer")) {
   423  		t.Fatalf("expected container cgroup path %q to be under pod cgroup path %q",
   424  			cm.Path("freezer"), pm.Path("freezer"))
   425  	}
   426  	// Check the container is not reported as frozen despite the frozen parent.
   427  	cf, err := cm.GetFreezerState()
   428  	if err != nil {
   429  		t.Fatal(err)
   430  	}
   431  	if cf != configs.Thawed {
   432  		t.Fatalf("expected container to be thawed, got %v", cf)
   433  	}
   434  
   435  	// Unfreeze the pod.
   436  	if err := pm.Freeze(configs.Thawed); err != nil {
   437  		t.Fatal(err)
   438  	}
   439  
   440  	cf, err = cm.GetFreezerState()
   441  	if err != nil {
   442  		t.Fatal(err)
   443  	}
   444  	if cf != configs.Thawed {
   445  		t.Fatalf("expected container to be thawed, got %v", cf)
   446  	}
   447  
   448  	// Check the "container" works.
   449  	marker := "one two\n"
   450  	_, err = stdinW.WriteString(marker)
   451  	if err != nil {
   452  		t.Fatal(err)
   453  	}
   454  	reply, err := rdr.ReadString('\n')
   455  	if err != nil {
   456  		t.Fatalf("reading from container: %v", err)
   457  	}
   458  	if reply != marker {
   459  		t.Fatalf("expected %q, got %q", marker, reply)
   460  	}
   461  }
   462  

View as plain text