Skip to content

Commit 698e149

Browse files
author
Jess Frazelle
committed
Merge pull request moby#16159 from runcom/validate-cpuset-cpus
Validate --cpuset-cpus, --cpuset-mems
2 parents bbac09a + 94464e3 commit 698e149

File tree

10 files changed

+303
-7
lines changed

10 files changed

+303
-7
lines changed

daemon/daemon_unix.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/Sirupsen/logrus"
1414
"github.com/docker/docker/autogen/dockerversion"
1515
"github.com/docker/docker/daemon/graphdriver"
16+
derr "github.com/docker/docker/errors"
1617
"github.com/docker/docker/pkg/fileutils"
1718
"github.com/docker/docker/pkg/parsers"
1819
"github.com/docker/docker/pkg/parsers/kernel"
@@ -194,6 +195,20 @@ func verifyPlatformContainerSettings(daemon *Daemon, hostConfig *runconfig.HostC
194195
hostConfig.CpusetCpus = ""
195196
hostConfig.CpusetMems = ""
196197
}
198+
cpusAvailable, err := sysInfo.IsCpusetCpusAvailable(hostConfig.CpusetCpus)
199+
if err != nil {
200+
return warnings, derr.ErrorCodeInvalidCpusetCpus.WithArgs(hostConfig.CpusetCpus)
201+
}
202+
if !cpusAvailable {
203+
return warnings, derr.ErrorCodeNotAvailableCpusetCpus.WithArgs(hostConfig.CpusetCpus, sysInfo.Cpus)
204+
}
205+
memsAvailable, err := sysInfo.IsCpusetMemsAvailable(hostConfig.CpusetMems)
206+
if err != nil {
207+
return warnings, derr.ErrorCodeInvalidCpusetMems.WithArgs(hostConfig.CpusetMems)
208+
}
209+
if !memsAvailable {
210+
return warnings, derr.ErrorCodeNotAvailableCpusetMems.WithArgs(hostConfig.CpusetMems, sysInfo.Mems)
211+
}
197212
if hostConfig.BlkioWeight > 0 && !sysInfo.BlkioWeight {
198213
warnings = append(warnings, "Your kernel does not support Block I/O weight. Weight discarded.")
199214
logrus.Warnf("Your kernel does not support Block I/O weight. Weight discarded.")
@@ -234,10 +249,7 @@ func checkSystem() error {
234249
if os.Geteuid() != 0 {
235250
return fmt.Errorf("The Docker daemon needs to be run as root")
236251
}
237-
if err := checkKernel(); err != nil {
238-
return err
239-
}
240-
return nil
252+
return checkKernel()
241253
}
242254

243255
// configureKernelSecuritySupport configures and validate security support for the kernel

errors/daemon.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,4 +827,40 @@ var (
827827
Description: "While trying to delete a container, there was an error trying to delete one of its volumes",
828828
HTTPStatusCode: http.StatusInternalServerError,
829829
})
830+
831+
// ErrorCodeInvalidCpusetCpus is generated when user provided cpuset CPUs
832+
// are invalid.
833+
ErrorCodeInvalidCpusetCpus = errcode.Register(errGroup, errcode.ErrorDescriptor{
834+
Value: "INVALIDCPUSETCPUS",
835+
Message: "Invalid value %s for cpuset cpus.",
836+
Description: "While verifying the container's 'HostConfig', CpusetCpus value was in an incorrect format",
837+
HTTPStatusCode: http.StatusInternalServerError,
838+
})
839+
840+
// ErrorCodeInvalidCpusetMems is generated when user provided cpuset mems
841+
// are invalid.
842+
ErrorCodeInvalidCpusetMems = errcode.Register(errGroup, errcode.ErrorDescriptor{
843+
Value: "INVALIDCPUSETMEMS",
844+
Message: "Invalid value %s for cpuset mems.",
845+
Description: "While verifying the container's 'HostConfig', CpusetMems value was in an incorrect format",
846+
HTTPStatusCode: http.StatusInternalServerError,
847+
})
848+
849+
// ErrorCodeNotAvailableCpusetCpus is generated when user provided cpuset
850+
// CPUs aren't available in the container's cgroup.
851+
ErrorCodeNotAvailableCpusetCpus = errcode.Register(errGroup, errcode.ErrorDescriptor{
852+
Value: "NOTAVAILABLECPUSETCPUS",
853+
Message: "Requested CPUs are not available - requested %s, available: %s.",
854+
Description: "While verifying the container's 'HostConfig', cpuset CPUs provided aren't available in the container's cgroup available set",
855+
HTTPStatusCode: http.StatusInternalServerError,
856+
})
857+
858+
// ErrorCodeNotAvailableCpusetMems is generated when user provided cpuset
859+
// memory nodes aren't available in the container's cgroup.
860+
ErrorCodeNotAvailableCpusetMems = errcode.Register(errGroup, errcode.ErrorDescriptor{
861+
Value: "NOTAVAILABLECPUSETMEMS",
862+
Message: "Requested memory nodes are not available - requested %s, available: %s.",
863+
Description: "While verifying the container's 'HostConfig', cpuset memory nodes provided aren't available in the container's cgroup available set",
864+
HTTPStatusCode: http.StatusInternalServerError,
865+
})
830866
)

integration-cli/docker_api_containers_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,3 +1525,29 @@ func (s *DockerSuite) TestContainersApiGetContainersJSONEmpty(c *check.C) {
15251525
c.Fatalf("Expected empty response to be `[]`, got %q", string(body))
15261526
}
15271527
}
1528+
1529+
func (s *DockerSuite) TestPostContainersCreateWithWrongCpusetValues(c *check.C) {
1530+
testRequires(c, DaemonIsLinux)
1531+
1532+
c1 := struct {
1533+
Image string
1534+
CpusetCpus string
1535+
}{"busybox", "1-42,,"}
1536+
name := "wrong-cpuset-cpus"
1537+
status, body, err := sockRequest("POST", "/containers/create?name="+name, c1)
1538+
c.Assert(err, check.IsNil)
1539+
c.Assert(status, check.Equals, http.StatusInternalServerError)
1540+
expected := "Invalid value 1-42,, for cpuset cpus.\n"
1541+
c.Assert(string(body), check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, string(body)))
1542+
1543+
c2 := struct {
1544+
Image string
1545+
CpusetMems string
1546+
}{"busybox", "42-3,1--"}
1547+
name = "wrong-cpuset-mems"
1548+
status, body, err = sockRequest("POST", "/containers/create?name="+name, c2)
1549+
c.Assert(err, check.IsNil)
1550+
c.Assert(status, check.Equals, http.StatusInternalServerError)
1551+
expected = "Invalid value 42-3,1-- for cpuset mems.\n"
1552+
c.Assert(string(body), check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, string(body)))
1553+
}

integration-cli/docker_cli_run_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3407,3 +3407,17 @@ func (s *DockerSuite) TestRunStdinBlockedAfterContainerExit(c *check.C) {
34073407
c.Fatal("timeout waiting for command to exit")
34083408
}
34093409
}
3410+
3411+
func (s *DockerSuite) TestRunWrongCpusetCpusFlagValue(c *check.C) {
3412+
out, _, err := dockerCmdWithError("run", "--cpuset-cpus", "1-10,11--", "busybox", "true")
3413+
c.Assert(err, check.NotNil)
3414+
expected := "Error response from daemon: Invalid value 1-10,11-- for cpuset cpus.\n"
3415+
c.Assert(out, check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, out))
3416+
}
3417+
3418+
func (s *DockerSuite) TestRunWrongCpusetMemsFlagValue(c *check.C) {
3419+
out, _, err := dockerCmdWithError("run", "--cpuset-mems", "1-42--", "busybox", "true")
3420+
c.Assert(err, check.NotNil)
3421+
expected := "Error response from daemon: Invalid value 1-42-- for cpuset mems.\n"
3422+
c.Assert(out, check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, out))
3423+
}

integration-cli/docker_cli_run_unix_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ import (
99
"os"
1010
"os/exec"
1111
"path/filepath"
12+
"strconv"
1213
"strings"
1314
"time"
1415

1516
"github.com/docker/docker/pkg/integration/checker"
1617
"github.com/docker/docker/pkg/mount"
18+
"github.com/docker/docker/pkg/parsers"
19+
"github.com/docker/docker/pkg/sysinfo"
1720
"github.com/go-check/check"
1821
"github.com/kr/pty"
1922
)
@@ -374,3 +377,41 @@ func (s *DockerSuite) TestRunSwapLessThanMemoryLimit(c *check.C) {
374377
c.Fatalf("Expected output to contain %q, not %q", out, expected)
375378
}
376379
}
380+
381+
func (s *DockerSuite) TestRunInvalidCpusetCpusFlagValue(c *check.C) {
382+
testRequires(c, cgroupCpuset)
383+
384+
sysInfo := sysinfo.New(true)
385+
cpus, err := parsers.ParseUintList(sysInfo.Cpus)
386+
c.Assert(err, check.IsNil)
387+
var invalid int
388+
for i := 0; i <= len(cpus)+1; i++ {
389+
if !cpus[i] {
390+
invalid = i
391+
break
392+
}
393+
}
394+
out, _, err := dockerCmdWithError("run", "--cpuset-cpus", strconv.Itoa(invalid), "busybox", "true")
395+
c.Assert(err, check.NotNil)
396+
expected := fmt.Sprintf("Error response from daemon: Requested CPUs are not available - requested %s, available: %s.\n", strconv.Itoa(invalid), sysInfo.Cpus)
397+
c.Assert(out, check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, out))
398+
}
399+
400+
func (s *DockerSuite) TestRunInvalidCpusetMemsFlagValue(c *check.C) {
401+
testRequires(c, cgroupCpuset)
402+
403+
sysInfo := sysinfo.New(true)
404+
mems, err := parsers.ParseUintList(sysInfo.Mems)
405+
c.Assert(err, check.IsNil)
406+
var invalid int
407+
for i := 0; i <= len(mems)+1; i++ {
408+
if !mems[i] {
409+
invalid = i
410+
break
411+
}
412+
}
413+
out, _, err := dockerCmdWithError("run", "--cpuset-mems", strconv.Itoa(invalid), "busybox", "true")
414+
c.Assert(err, check.NotNil)
415+
expected := fmt.Sprintf("Error response from daemon: Requested memory nodes are not available - requested %s, available: %s.\n", strconv.Itoa(invalid), sysInfo.Mems)
416+
c.Assert(out, check.Equals, expected, check.Commentf("Expected output to contain %q, got %q", expected, out))
417+
}

pkg/parsers/parsers.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func PartParser(template, data string) (map[string]string, error) {
127127
out = make(map[string]string, len(templateParts))
128128
)
129129
if len(parts) != len(templateParts) {
130-
return nil, fmt.Errorf("Invalid format to parse. %s should match template %s", data, template)
130+
return nil, fmt.Errorf("Invalid format to parse. %s should match template %s", data, template)
131131
}
132132

133133
for i, t := range templateParts {
@@ -196,3 +196,53 @@ func ParseLink(val string) (string, string, error) {
196196
}
197197
return arr[0], arr[1], nil
198198
}
199+
200+
// ParseUintList parses and validates the specified string as the value
201+
// found in some cgroup file (e.g. `cpuset.cpus`, `cpuset.mems`), which could be
202+
// one of the formats below. Note that duplicates are actually allowed in the
203+
// input string. It returns a `map[int]bool` with available elements from `val`
204+
// set to `true`.
205+
// Supported formats:
206+
// 7
207+
// 1-6
208+
// 0,3-4,7,8-10
209+
// 0-0,0,1-7
210+
// 03,1-3 <- this is gonna get parsed as [1,2,3]
211+
// 3,2,1
212+
// 0-2,3,1
213+
func ParseUintList(val string) (map[int]bool, error) {
214+
if val == "" {
215+
return map[int]bool{}, nil
216+
}
217+
218+
availableInts := make(map[int]bool)
219+
split := strings.Split(val, ",")
220+
errInvalidFormat := fmt.Errorf("invalid format: %s", val)
221+
222+
for _, r := range split {
223+
if !strings.Contains(r, "-") {
224+
v, err := strconv.Atoi(r)
225+
if err != nil {
226+
return nil, errInvalidFormat
227+
}
228+
availableInts[v] = true
229+
} else {
230+
split := strings.SplitN(r, "-", 2)
231+
min, err := strconv.Atoi(split[0])
232+
if err != nil {
233+
return nil, errInvalidFormat
234+
}
235+
max, err := strconv.Atoi(split[1])
236+
if err != nil {
237+
return nil, errInvalidFormat
238+
}
239+
if max < min {
240+
return nil, errInvalidFormat
241+
}
242+
for i := min; i <= max; i++ {
243+
availableInts[i] = true
244+
}
245+
}
246+
}
247+
return availableInts, nil
248+
}

pkg/parsers/parsers_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package parsers
22

33
import (
4+
"reflect"
45
"runtime"
56
"strings"
67
"testing"
@@ -238,3 +239,40 @@ func TestParseLink(t *testing.T) {
238239
t.Fatalf("Expected error 'bad format for links: link:alias:wrong' but got: %v", err)
239240
}
240241
}
242+
243+
func TestParseUintList(t *testing.T) {
244+
valids := map[string]map[int]bool{
245+
"": {},
246+
"7": {7: true},
247+
"1-6": {1: true, 2: true, 3: true, 4: true, 5: true, 6: true},
248+
"0-7": {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true},
249+
"0,3-4,7,8-10": {0: true, 3: true, 4: true, 7: true, 8: true, 9: true, 10: true},
250+
"0-0,0,1-4": {0: true, 1: true, 2: true, 3: true, 4: true},
251+
"03,1-3": {1: true, 2: true, 3: true},
252+
"3,2,1": {1: true, 2: true, 3: true},
253+
"0-2,3,1": {0: true, 1: true, 2: true, 3: true},
254+
}
255+
for k, v := range valids {
256+
out, err := ParseUintList(k)
257+
if err != nil {
258+
t.Fatalf("Expected not to fail, got %v", err)
259+
}
260+
if !reflect.DeepEqual(out, v) {
261+
t.Fatalf("Expected %v, got %v", v, out)
262+
}
263+
}
264+
265+
invalids := []string{
266+
"this",
267+
"1--",
268+
"1-10,,10",
269+
"10-1",
270+
"-1",
271+
"-1,0",
272+
}
273+
for _, v := range invalids {
274+
if out, err := ParseUintList(v); err == nil {
275+
t.Fatalf("Expected failure with %s but got %v", v, out)
276+
}
277+
}
278+
}

pkg/sysinfo/sysinfo.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package sysinfo
22

3+
import "github.com/docker/docker/pkg/parsers"
4+
35
// SysInfo stores information about which features a kernel supports.
46
// TODO Windows: Factor out platform specific capabilities.
57
type SysInfo struct {
@@ -63,4 +65,41 @@ type cgroupBlkioInfo struct {
6365
type cgroupCpusetInfo struct {
6466
// Whether Cpuset is supported or not
6567
Cpuset bool
68+
69+
// Available Cpuset's cpus
70+
Cpus string
71+
72+
// Available Cpuset's memory nodes
73+
Mems string
74+
}
75+
76+
// IsCpusetCpusAvailable returns `true` if the provided string set is contained
77+
// in cgroup's cpuset.cpus set, `false` otherwise.
78+
// If error is not nil a parsing error occurred.
79+
func (c cgroupCpusetInfo) IsCpusetCpusAvailable(provided string) (bool, error) {
80+
return isCpusetListAvailable(provided, c.Cpus)
81+
}
82+
83+
// IsCpusetMemsAvailable returns `true` if the provided string set is contained
84+
// in cgroup's cpuset.mems set, `false` otherwise.
85+
// If error is not nil a parsing error occurred.
86+
func (c cgroupCpusetInfo) IsCpusetMemsAvailable(provided string) (bool, error) {
87+
return isCpusetListAvailable(provided, c.Mems)
88+
}
89+
90+
func isCpusetListAvailable(provided, available string) (bool, error) {
91+
parsedProvided, err := parsers.ParseUintList(provided)
92+
if err != nil {
93+
return false, err
94+
}
95+
parsedAvailable, err := parsers.ParseUintList(available)
96+
if err != nil {
97+
return false, err
98+
}
99+
for k := range parsedProvided {
100+
if !parsedAvailable[k] {
101+
return false, nil
102+
}
103+
}
104+
return true, nil
66105
}

pkg/sysinfo/sysinfo_linux.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,29 @@ func checkCgroupBlkioInfo(quiet bool) cgroupBlkioInfo {
126126

127127
// checkCgroupCpusetInfo reads the cpuset information from the cpuset cgroup mount point.
128128
func checkCgroupCpusetInfo(quiet bool) cgroupCpusetInfo {
129-
_, err := cgroups.FindCgroupMountpoint("cpuset")
129+
mountPoint, err := cgroups.FindCgroupMountpoint("cpuset")
130130
if err != nil {
131131
if !quiet {
132132
logrus.Warn(err)
133133
}
134134
return cgroupCpusetInfo{}
135135
}
136136

137-
return cgroupCpusetInfo{Cpuset: true}
137+
cpus, err := ioutil.ReadFile(path.Join(mountPoint, "cpuset.cpus"))
138+
if err != nil {
139+
return cgroupCpusetInfo{}
140+
}
141+
142+
mems, err := ioutil.ReadFile(path.Join(mountPoint, "cpuset.mems"))
143+
if err != nil {
144+
return cgroupCpusetInfo{}
145+
}
146+
147+
return cgroupCpusetInfo{
148+
Cpuset: true,
149+
Cpus: strings.TrimSpace(string(cpus)),
150+
Mems: strings.TrimSpace(string(mems)),
151+
}
138152
}
139153

140154
func cgroupEnabled(mountPoint, name string) bool {

0 commit comments

Comments
 (0)