Merge pull request #186 from vishh/fix_godeps

Updating libcontainer dependency via 'godep update'.
This commit is contained in:
Victor Marmol 2014-08-19 15:51:24 -04:00
commit a80c3b81a8
64 changed files with 1498 additions and 710 deletions

4
Godeps/Godeps.json generated
View File

@ -9,8 +9,8 @@
}, },
{ {
"ImportPath": "github.com/docker/libcontainer", "ImportPath": "github.com/docker/libcontainer",
"Comment": "v1.1.0-115-ge6a43c1", "Comment": "v1.1.0-194-gedfe81a",
"Rev": "e6a43c1c2b9f769deb96348a0a93417cd48a36d8" "Rev": "edfe81a08b2780ad75b63e60b6cb9eb3a17c671f"
}, },
{ {
"ImportPath": "github.com/fsouza/go-dockerclient", "ImportPath": "github.com/fsouza/go-dockerclient",

View File

@ -13,22 +13,24 @@ env:
- _GOOS=linux _GOARCH=arm CGO_ENABLED=0 - _GOOS=linux _GOARCH=arm CGO_ENABLED=0
install: install:
- go get code.google.com/p/go.tools/cmd/cover
- mkdir -pv "${GOPATH%%:*}/src/github.com/docker" && [ -d "${GOPATH%%:*}/src/github.com/docker/libcontainer" ] || ln -sv "$(readlink -f .)" "${GOPATH%%:*}/src/github.com/docker/libcontainer" - mkdir -pv "${GOPATH%%:*}/src/github.com/docker" && [ -d "${GOPATH%%:*}/src/github.com/docker/libcontainer" ] || ln -sv "$(readlink -f .)" "${GOPATH%%:*}/src/github.com/docker/libcontainer"
- if [ -z "$TRAVIS_GLOBAL_WTF" ]; then - if [ -z "$TRAVIS_GLOBAL_WTF" ]; then
gvm cross "$_GOOS" "$_GOARCH"; gvm cross "$_GOOS" "$_GOARCH";
export GOOS="$_GOOS" GOARCH="$_GOARCH"; export GOOS="$_GOOS" GOARCH="$_GOARCH";
fi fi
- export GOPATH="$GOPATH:$(pwd)/vendor"
- if [ -z "$TRAVIS_GLOBAL_WTF" ]; then go env; fi - if [ -z "$TRAVIS_GLOBAL_WTF" ]; then go env; fi
- go get -d -v ./... - go get -d -v ./... # TODO remove this if /docker/docker gets purged from our includes
- if [ "$TRAVIS_GLOBAL_WTF" ]; then - if [ "$TRAVIS_GLOBAL_WTF" ]; then
export DOCKER_PATH="${GOPATH%%:*}/src/github.com/dotcloud/docker"; export DOCKER_PATH="${GOPATH%%:*}/src/github.com/docker/docker";
mkdir -p "$DOCKER_PATH/hack/make"; mkdir -p "$DOCKER_PATH/hack/make";
( cd "$DOCKER_PATH/hack/make" && wget -c 'https://raw.githubusercontent.com/dotcloud/docker/master/hack/make/'{.validate,validate-dco,validate-gofmt} ); ( cd "$DOCKER_PATH/hack/make" && wget -c 'https://raw.githubusercontent.com/docker/docker/master/hack/make/'{.validate,validate-dco,validate-gofmt} );
sed -i 's!dotcloud/docker!docker/libcontainer!' "$DOCKER_PATH/hack/make/.validate"; sed -i 's!docker/docker!docker/libcontainer!' "$DOCKER_PATH/hack/make/.validate";
fi fi
script: script:
- if [ "$TRAVIS_GLOBAL_WTF" ]; then bash "$DOCKER_PATH/hack/make/validate-dco"; fi - if [ "$TRAVIS_GLOBAL_WTF" ]; then bash "$DOCKER_PATH/hack/make/validate-dco"; fi
- if [ "$TRAVIS_GLOBAL_WTF" ]; then bash "$DOCKER_PATH/hack/make/validate-gofmt"; fi - if [ "$TRAVIS_GLOBAL_WTF" ]; then bash "$DOCKER_PATH/hack/make/validate-gofmt"; fi
- if [ -z "$TRAVIS_GLOBAL_WTF" ]; then go build -v ./...; fi - if [ -z "$TRAVIS_GLOBAL_WTF" ]; then make direct-build; fi
- if [ -z "$TRAVIS_GLOBAL_WTF" -a "$GOARCH" != 'arm' ]; then go test -test.short -v ./...; fi - if [ -z "$TRAVIS_GLOBAL_WTF" -a "$GOARCH" != 'arm' ]; then make direct-test-short; fi

View File

@ -176,7 +176,7 @@ One way to automate this, is customise your get ``commit.template`` by adding
a ``prepare-commit-msg`` hook to your libcontainer checkout: a ``prepare-commit-msg`` hook to your libcontainer checkout:
``` ```
curl -o .git/hooks/prepare-commit-msg https://raw.githubusercontent.com/dotcloud/docker/master/contrib/prepare-commit-msg.hook && chmod +x .git/hooks/prepare-commit-msg curl -o .git/hooks/prepare-commit-msg https://raw.githubusercontent.com/docker/docker/master/contrib/prepare-commit-msg.hook && chmod +x .git/hooks/prepare-commit-msg
``` ```
* Note: the above script expects to find your GitHub user name in ``git config --get github.user`` * Note: the above script expects to find your GitHub user name in ``git config --get github.user``

View File

@ -1,6 +1,6 @@
FROM crosbymichael/golang FROM crosbymichael/golang
RUN apt-get update && apt-get install -y gcc RUN apt-get update && apt-get install -y gcc make
RUN go get code.google.com/p/go.tools/cmd/cover RUN go get code.google.com/p/go.tools/cmd/cover
# setup a playground for us to spawn containers in # setup a playground for us to spawn containers in
@ -14,8 +14,10 @@ COPY . /go/src/github.com/docker/libcontainer
WORKDIR /go/src/github.com/docker/libcontainer WORKDIR /go/src/github.com/docker/libcontainer
RUN cp sample_configs/minimal.json /busybox/container.json RUN cp sample_configs/minimal.json /busybox/container.json
ENV GOPATH $GOPATH:/go/src/github.com/docker/libcontainer/vendor
RUN go get -d -v ./... RUN go get -d -v ./...
RUN go install -v ./... RUN make direct-install
ENTRYPOINT ["/dind"] ENTRYPOINT ["/dind"]
CMD ["go", "test", "-cover", "./..."] CMD ["make", "direct-test"]

View File

@ -1,4 +1,6 @@
Michael Crosby <michael@docker.com> (@crosbymichael) Michael Crosby <michael@docker.com> (@crosbymichael)
Rohit Jnagal <jnagal@google.com> (@rjnagal) Rohit Jnagal <jnagal@google.com> (@rjnagal)
Victor Marmol <vmarmol@google.com> (@vmarmol) Victor Marmol <vmarmol@google.com> (@vmarmol)
Mrunal Patel <mpatel@redhat.com> (@mrunalp)
.travis.yml: Tianon Gravi <admwiggin@gmail.com> (@tianon) .travis.yml: Tianon Gravi <admwiggin@gmail.com> (@tianon)
update-vendor.sh: Tianon Gravi <admwiggin@gmail.com> (@tianon)

View File

@ -2,9 +2,23 @@
all: all:
docker build -t docker/libcontainer . docker build -t docker/libcontainer .
test: test:
# we need NET_ADMIN for the netlink tests and SYS_ADMIN for mounting # we need NET_ADMIN for the netlink tests and SYS_ADMIN for mounting
docker run --rm --cap-add NET_ADMIN --cap-add SYS_ADMIN docker/libcontainer docker run --rm -it --cap-add NET_ADMIN --cap-add SYS_ADMIN docker/libcontainer
sh: sh:
docker run --rm -ti -w /busybox --rm --cap-add NET_ADMIN --cap-add SYS_ADMIN docker/libcontainer nsinit exec sh docker run --rm -it --cap-add NET_ADMIN --cap-add SYS_ADMIN -w /busybox docker/libcontainer nsinit exec sh
GO_PACKAGES = $(shell find . -not \( -wholename ./vendor -prune \) -name '*.go' -print0 | xargs -0n1 dirname | sort -u)
direct-test:
go test -cover -v $(GO_PACKAGES)
direct-test-short:
go test -cover -test.short -v $(GO_PACKAGES)
direct-build:
go build -v $(GO_PACKAGES)
direct-install:
go install -v $(GO_PACKAGES)

View File

@ -1,4 +1,4 @@
## libcontainer - reference implementation for containers ## libcontainer - reference implementation for containers [![Build Status](https://travis-ci.org/docker/libcontainer.png?branch=master)](https://travis-ci.org/docker/libcontainer)
### Note on API changes: ### Note on API changes:

View File

@ -37,4 +37,5 @@ type Cgroup struct {
type ActiveCgroup interface { type ActiveCgroup interface {
Cleanup() error Cleanup() error
Paths() (map[string]string, error)
} }

View File

@ -18,8 +18,8 @@ var createCommand = cli.Command{
Name: "create", Name: "create",
Usage: "Create a cgroup container using the supplied configuration and initial process.", Usage: "Create a cgroup container using the supplied configuration and initial process.",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{"config, c", "cgroup.json", "path to container configuration (cgroups.Cgroup object)"}, cli.StringFlag{Name: "config, c", Value: "cgroup.json", Usage: "path to container configuration (cgroups.Cgroup object)"},
cli.IntFlag{"pid, p", 0, "pid of the initial process in the container"}, cli.IntFlag{Name: "pid, p", Value: 0, Usage: "pid of the initial process in the container"},
}, },
Action: createAction, Action: createAction,
} }
@ -28,8 +28,8 @@ var destroyCommand = cli.Command{
Name: "destroy", Name: "destroy",
Usage: "Destroy an existing cgroup container.", Usage: "Destroy an existing cgroup container.",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{"name, n", "", "container name"}, cli.StringFlag{Name: "name, n", Value: "", Usage: "container name"},
cli.StringFlag{"parent, p", "", "container parent"}, cli.StringFlag{Name: "parent, p", Value: "", Usage: "container parent"},
}, },
Action: destroyAction, Action: destroyAction,
} }
@ -38,8 +38,8 @@ var statsCommand = cli.Command{
Name: "stats", Name: "stats",
Usage: "Get stats for cgroup", Usage: "Get stats for cgroup",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{"name, n", "", "container name"}, cli.StringFlag{Name: "name, n", Value: "", Usage: "container name"},
cli.StringFlag{"parent, p", "", "container parent"}, cli.StringFlag{Name: "parent, p", Value: "", Usage: "container parent"},
}, },
Action: statsAction, Action: statsAction,
} }
@ -48,8 +48,8 @@ var pauseCommand = cli.Command{
Name: "pause", Name: "pause",
Usage: "Pause cgroup", Usage: "Pause cgroup",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{"name, n", "", "container name"}, cli.StringFlag{Name: "name, n", Value: "", Usage: "container name"},
cli.StringFlag{"parent, p", "", "container parent"}, cli.StringFlag{Name: "parent, p", Value: "", Usage: "container parent"},
}, },
Action: pauseAction, Action: pauseAction,
} }
@ -58,8 +58,8 @@ var resumeCommand = cli.Command{
Name: "resume", Name: "resume",
Usage: "Resume a paused cgroup", Usage: "Resume a paused cgroup",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{"name, n", "", "container name"}, cli.StringFlag{Name: "name, n", Value: "", Usage: "container name"},
cli.StringFlag{"parent, p", "", "container parent"}, cli.StringFlag{Name: "parent, p", Value: "", Usage: "container parent"},
}, },
Action: resumeAction, Action: resumeAction,
} }
@ -68,8 +68,8 @@ var psCommand = cli.Command{
Name: "ps", Name: "ps",
Usage: "Get list of pids for a cgroup", Usage: "Get list of pids for a cgroup",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{"name, n", "", "container name"}, cli.StringFlag{Name: "name, n", Value: "", Usage: "container name"},
cli.StringFlag{"parent, p", "", "container parent"}, cli.StringFlag{Name: "parent, p", Value: "", Usage: "container parent"},
}, },
Action: psAction, Action: psAction,
} }

View File

@ -21,12 +21,16 @@ var (
"perf_event": &PerfEventGroup{}, "perf_event": &PerfEventGroup{},
"freezer": &FreezerGroup{}, "freezer": &FreezerGroup{},
} }
CgroupProcesses = "cgroup.procs"
) )
type subsystem interface { type subsystem interface {
Set(*data) error // Returns the stats, as 'stats', corresponding to the cgroup under 'path'.
GetStats(path string, stats *cgroups.Stats) error
// Removes the cgroup represented by 'data'.
Remove(*data) error Remove(*data) error
GetStats(string, *cgroups.Stats) error // Creates and joins the cgroup represented by data.
Set(*data) error
} }
type data struct { type data struct {
@ -149,7 +153,23 @@ func (raw *data) parent(subsystem string) (string, error) {
return filepath.Join(raw.root, subsystem, initPath), nil return filepath.Join(raw.root, subsystem, initPath), nil
} }
func (raw *data) Paths() (map[string]string, error) {
paths := make(map[string]string)
for sysname := range subsystems {
path, err := raw.path(sysname)
if err != nil {
return nil, err
}
paths[sysname] = path
}
return paths, nil
}
func (raw *data) path(subsystem string) (string, error) { func (raw *data) path(subsystem string) (string, error) {
// If the cgroup name/path is absolute do not look relative to the cgroup of the init process.
if filepath.IsAbs(raw.cgroup) {
return filepath.Join(raw.root, subsystem, raw.cgroup), nil
}
parent, err := raw.parent(subsystem) parent, err := raw.parent(subsystem)
if err != nil { if err != nil {
return "", err return "", err
@ -165,7 +185,7 @@ func (raw *data) join(subsystem string) (string, error) {
if err := os.MkdirAll(path, 0755); err != nil && !os.IsExist(err) { if err := os.MkdirAll(path, 0755); err != nil && !os.IsExist(err) {
return "", err return "", err
} }
if err := writeFile(path, "cgroup.procs", strconv.Itoa(raw.pid)); err != nil { if err := writeFile(path, CgroupProcesses, strconv.Itoa(raw.pid)); err != nil {
return "", err return "", err
} }
return path, nil return path, nil

View File

@ -54,7 +54,7 @@ func (s *CpuacctGroup) GetStats(path string, stats *cgroups.Stats) error {
return err return err
} }
// sample for 100ms // sample for 100ms
time.Sleep(100 * time.Millisecond) time.Sleep(1000 * time.Millisecond)
if kernelModeUsage, userModeUsage, err = getCpuUsage(path); err != nil { if kernelModeUsage, userModeUsage, err = getCpuUsage(path); err != nil {
return err return err
} }
@ -73,7 +73,7 @@ func (s *CpuacctGroup) GetStats(path string, stats *cgroups.Stats) error {
deltaUsage = lastUsage - startUsage deltaUsage = lastUsage - startUsage
) )
if deltaSystem > 0.0 { if deltaSystem > 0.0 {
percentage = ((deltaProc / deltaSystem) * clockTicks) * cpuCount percentage = uint64((float64(deltaProc) / float64(deltaSystem)) * float64(clockTicks*cpuCount))
} }
// NOTE: a percentage over 100% is valid for POSIX because that means the // NOTE: a percentage over 100% is valid for POSIX because that means the
// processes is using multiple cores // processes is using multiple cores

View File

@ -20,19 +20,10 @@ func (s *CpusetGroup) Set(d *data) error {
if err != nil { if err != nil {
return err return err
} }
if err := s.ensureParent(dir); err != nil {
return err
}
// because we are not using d.join we need to place the pid into the procs file return s.SetDir(dir, d.c.CpusetCpus, d.pid)
// unlike the other subsystems
if err := writeFile(dir, "cgroup.procs", strconv.Itoa(d.pid)); err != nil {
return err
}
if err := writeFile(dir, "cpuset.cpus", d.c.CpusetCpus); err != nil {
return err
}
} }
return nil return nil
} }
@ -44,6 +35,24 @@ func (s *CpusetGroup) GetStats(path string, stats *cgroups.Stats) error {
return nil return nil
} }
func (s *CpusetGroup) SetDir(dir, value string, pid int) error {
if err := s.ensureParent(dir); err != nil {
return err
}
// because we are not using d.join we need to place the pid into the procs file
// unlike the other subsystems
if err := writeFile(dir, "cgroup.procs", strconv.Itoa(pid)); err != nil {
return err
}
if err := writeFile(dir, "cpuset.cpus", value); err != nil {
return err
}
return nil
}
func (s *CpusetGroup) getSubsystemSettings(parent string) (cpus []byte, mems []byte, err error) { func (s *CpusetGroup) getSubsystemSettings(parent string) (cpus []byte, mems []byte, err error) {
if cpus, err = ioutil.ReadFile(filepath.Join(parent, "cpuset.cpus")); err != nil { if cpus, err = ioutil.ReadFile(filepath.Join(parent, "cpuset.cpus")); err != nil {
return return

Binary file not shown.

View File

@ -14,7 +14,7 @@ type MemoryGroup struct {
func (s *MemoryGroup) Set(d *data) error { func (s *MemoryGroup) Set(d *data) error {
dir, err := d.join("memory") dir, err := d.join("memory")
// only return an error for memory if it was not specified // only return an error for memory if it was specified
if err != nil && (d.c.Memory != 0 || d.c.MemoryReservation != 0 || d.c.MemorySwap != 0) { if err != nil && (d.c.Memory != 0 || d.c.MemoryReservation != 0 || d.c.MemorySwap != 0) {
return err return err
} }

View File

@ -5,6 +5,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/docker/libcontainer/cgroups"
) )
const ( const (
@ -66,3 +68,20 @@ func TestGetCgroupParamsInt(t *testing.T) {
t.Fatal("Expecting error, got none") t.Fatal("Expecting error, got none")
} }
} }
func TestAbsolutePathHandling(t *testing.T) {
testCgroup := cgroups.Cgroup{
Name: "bar",
Parent: "/foo",
}
cgroupData := data{
root: "/sys/fs/cgroup",
cgroup: "/foo/bar",
c: &testCgroup,
pid: 1,
}
expectedPath := filepath.Join(cgroupData.root, "cpu", testCgroup.Parent, testCgroup.Name)
if path, err := cgroupData.path("cpu"); path != expectedPath || err != nil {
t.Fatalf("expected path %s but got %s %s", expectedPath, path, err)
}
}

View File

@ -13,15 +13,14 @@ import (
"sync" "sync"
"time" "time"
systemd1 "github.com/coreos/go-systemd/dbus" systemd "github.com/coreos/go-systemd/dbus"
"github.com/docker/docker/pkg/systemd"
"github.com/docker/libcontainer/cgroups" "github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/cgroups/fs" "github.com/docker/libcontainer/cgroups/fs"
"github.com/godbus/dbus" "github.com/godbus/dbus"
) )
type systemdCgroup struct { type systemdCgroup struct {
cleanupDirs []string cgroup *cgroups.Cgroup
} }
type subsystem interface { type subsystem interface {
@ -30,7 +29,7 @@ type subsystem interface {
var ( var (
connLock sync.Mutex connLock sync.Mutex
theConn *systemd1.Conn theConn *systemd.Conn
hasStartTransientUnit bool hasStartTransientUnit bool
subsystems = map[string]subsystem{ subsystems = map[string]subsystem{
"devices": &fs.DevicesGroup{}, "devices": &fs.DevicesGroup{},
@ -45,7 +44,8 @@ var (
) )
func UseSystemd() bool { func UseSystemd() bool {
if !systemd.SdBooted() { s, err := os.Stat("/run/systemd/system")
if err != nil || !s.IsDir() {
return false return false
} }
@ -54,7 +54,7 @@ func UseSystemd() bool {
if theConn == nil { if theConn == nil {
var err error var err error
theConn, err = systemd1.New() theConn, err = systemd.New()
if err != nil { if err != nil {
return false return false
} }
@ -84,267 +84,126 @@ func getIfaceForUnit(unitName string) string {
return "Unit" return "Unit"
} }
type cgroupArg struct {
File string
Value string
}
func Apply(c *cgroups.Cgroup, pid int) (cgroups.ActiveCgroup, error) { func Apply(c *cgroups.Cgroup, pid int) (cgroups.ActiveCgroup, error) {
var ( var (
unitName = getUnitName(c) unitName = getUnitName(c)
slice = "system.slice" slice = "system.slice"
properties []systemd1.Property properties []systemd.Property
cpuArgs []cgroupArg res = &systemdCgroup{}
cpusetArgs []cgroupArg
memoryArgs []cgroupArg
res systemdCgroup
) )
// First set up things not supported by systemd res.cgroup = c
// -1 disables memorySwap
if c.MemorySwap >= 0 && (c.Memory != 0 || c.MemorySwap > 0) {
memorySwap := c.MemorySwap
if memorySwap == 0 {
// By default, MemorySwap is set to twice the size of RAM.
memorySwap = c.Memory * 2
}
memoryArgs = append(memoryArgs, cgroupArg{"memory.memsw.limit_in_bytes", strconv.FormatInt(memorySwap, 10)})
}
if c.CpusetCpus != "" {
cpusetArgs = append(cpusetArgs, cgroupArg{"cpuset.cpus", c.CpusetCpus})
}
if c.Slice != "" { if c.Slice != "" {
slice = c.Slice slice = c.Slice
} }
properties = append(properties, properties = append(properties,
systemd1.Property{"Slice", dbus.MakeVariant(slice)}, systemd.Property{"Slice", dbus.MakeVariant(slice)},
systemd1.Property{"Description", dbus.MakeVariant("docker container " + c.Name)}, systemd.Property{"Description", dbus.MakeVariant("docker container " + c.Name)},
systemd1.Property{"PIDs", dbus.MakeVariant([]uint32{uint32(pid)})}, systemd.Property{"PIDs", dbus.MakeVariant([]uint32{uint32(pid)})},
) )
// Always enable accounting, this gets us the same behaviour as the fs implementation, // Always enable accounting, this gets us the same behaviour as the fs implementation,
// plus the kernel has some problems with joining the memory cgroup at a later time. // plus the kernel has some problems with joining the memory cgroup at a later time.
properties = append(properties, properties = append(properties,
systemd1.Property{"MemoryAccounting", dbus.MakeVariant(true)}, systemd.Property{"MemoryAccounting", dbus.MakeVariant(true)},
systemd1.Property{"CPUAccounting", dbus.MakeVariant(true)}, systemd.Property{"CPUAccounting", dbus.MakeVariant(true)},
systemd1.Property{"BlockIOAccounting", dbus.MakeVariant(true)}) systemd.Property{"BlockIOAccounting", dbus.MakeVariant(true)})
if c.Memory != 0 { if c.Memory != 0 {
properties = append(properties, properties = append(properties,
systemd1.Property{"MemoryLimit", dbus.MakeVariant(uint64(c.Memory))}) systemd.Property{"MemoryLimit", dbus.MakeVariant(uint64(c.Memory))})
} }
// TODO: MemoryReservation and MemorySwap not available in systemd // TODO: MemoryReservation and MemorySwap not available in systemd
if c.CpuShares != 0 { if c.CpuShares != 0 {
properties = append(properties, properties = append(properties,
systemd1.Property{"CPUShares", dbus.MakeVariant(uint64(c.CpuShares))}) systemd.Property{"CPUShares", dbus.MakeVariant(uint64(c.CpuShares))})
} }
if _, err := theConn.StartTransientUnit(unitName, "replace", properties...); err != nil { if _, err := theConn.StartTransientUnit(unitName, "replace", properties...); err != nil {
return nil, err return nil, err
} }
// To work around the lack of /dev/pts/* support above we need to manually add these
// so, ask systemd for the cgroup used
props, err := theConn.GetUnitTypeProperties(unitName, getIfaceForUnit(unitName))
if err != nil {
return nil, err
}
cgroup := props["ControlGroup"].(string)
if !c.AllowAllDevices { if !c.AllowAllDevices {
// Atm we can't use the systemd device support because of two missing things: if err := joinDevices(c, pid); err != nil {
// * Support for wildcards to allow mknod on any device
// * Support for wildcards to allow /dev/pts support
//
// The second is available in more recent systemd as "char-pts", but not in e.g. v208 which is
// in wide use. When both these are availalable we will be able to switch, but need to keep the old
// implementation for backwards compat.
//
// Note: we can't use systemd to set up the initial limits, and then change the cgroup
// because systemd will re-write the device settings if it needs to re-apply the cgroup context.
// This happens at least for v208 when any sibling unit is started.
mountpoint, err := cgroups.FindCgroupMountpoint("devices")
if err != nil {
return nil, err return nil, err
} }
initPath, err := cgroups.GetInitCgroupDir("devices")
if err != nil {
return nil, err
}
dir := filepath.Join(mountpoint, initPath, c.Parent, c.Name)
res.cleanupDirs = append(res.cleanupDirs, dir)
if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist(err) {
return nil, err
}
if err := ioutil.WriteFile(filepath.Join(dir, "cgroup.procs"), []byte(strconv.Itoa(pid)), 0700); err != nil {
return nil, err
}
if err := writeFile(dir, "devices.deny", "a"); err != nil {
return nil, err
}
for _, dev := range c.AllowedDevices {
if err := writeFile(dir, "devices.allow", dev.GetCgroupAllowString()); err != nil {
return nil, err
}
}
} }
if len(cpuArgs) != 0 { // -1 disables memorySwap
mountpoint, err := cgroups.FindCgroupMountpoint("cpu") if c.MemorySwap >= 0 && (c.Memory != 0 || c.MemorySwap > 0) {
if err != nil { if err := joinMemory(c, pid); err != nil {
return nil, err return nil, err
} }
path := filepath.Join(mountpoint, cgroup)
for _, arg := range cpuArgs {
if err := ioutil.WriteFile(filepath.Join(path, arg.File), []byte(arg.Value), 0700); err != nil {
return nil, err
}
}
}
if len(memoryArgs) != 0 {
mountpoint, err := cgroups.FindCgroupMountpoint("memory")
if err != nil {
return nil, err
}
path := filepath.Join(mountpoint, cgroup)
for _, arg := range memoryArgs {
if err := ioutil.WriteFile(filepath.Join(path, arg.File), []byte(arg.Value), 0700); err != nil {
return nil, err
}
}
} }
// we need to manually join the freezer cgroup in systemd because it does not currently support it // we need to manually join the freezer cgroup in systemd because it does not currently support it
// via the dbus api // via the dbus api
freezerPath, err := joinFreezer(c, pid) if err := joinFreezer(c, pid); err != nil {
if err != nil {
return nil, err return nil, err
} }
res.cleanupDirs = append(res.cleanupDirs, freezerPath)
if len(cpusetArgs) != 0 { if c.CpusetCpus != "" {
// systemd does not atm set up the cpuset controller, so we must manually if err := joinCpuset(c, pid); err != nil {
// join it. Additionally that is a very finicky controller where each
// level must have a full setup as the default for a new directory is "no cpus",
// so we avoid using any hierarchies here, creating a toplevel directory.
mountpoint, err := cgroups.FindCgroupMountpoint("cpuset")
if err != nil {
return nil, err
}
initPath, err := cgroups.GetInitCgroupDir("cpuset")
if err != nil {
return nil, err
}
var (
foundCpus bool
foundMems bool
rootPath = filepath.Join(mountpoint, initPath)
path = filepath.Join(mountpoint, initPath, c.Parent+"-"+c.Name)
)
res.cleanupDirs = append(res.cleanupDirs, path)
if err := os.MkdirAll(path, 0755); err != nil && !os.IsExist(err) {
return nil, err
}
for _, arg := range cpusetArgs {
if arg.File == "cpuset.cpus" {
foundCpus = true
}
if arg.File == "cpuset.mems" {
foundMems = true
}
if err := ioutil.WriteFile(filepath.Join(path, arg.File), []byte(arg.Value), 0700); err != nil {
return nil, err
}
}
// These are required, if not specified inherit from parent
if !foundCpus {
s, err := ioutil.ReadFile(filepath.Join(rootPath, "cpuset.cpus"))
if err != nil {
return nil, err
}
if err := ioutil.WriteFile(filepath.Join(path, "cpuset.cpus"), s, 0700); err != nil {
return nil, err
}
}
// These are required, if not specified inherit from parent
if !foundMems {
s, err := ioutil.ReadFile(filepath.Join(rootPath, "cpuset.mems"))
if err != nil {
return nil, err
}
if err := ioutil.WriteFile(filepath.Join(path, "cpuset.mems"), s, 0700); err != nil {
return nil, err
}
}
if err := ioutil.WriteFile(filepath.Join(path, "cgroup.procs"), []byte(strconv.Itoa(pid)), 0700); err != nil {
return nil, err return nil, err
} }
} }
return &res, nil return res, nil
} }
func writeFile(dir, file, data string) error { func writeFile(dir, file, data string) error {
return ioutil.WriteFile(filepath.Join(dir, file), []byte(data), 0700) return ioutil.WriteFile(filepath.Join(dir, file), []byte(data), 0700)
} }
func (c *systemdCgroup) Paths() (map[string]string, error) {
paths := make(map[string]string)
for sysname := range subsystems {
subsystemPath, err := getSubsystemPath(c.cgroup, sysname)
if err != nil {
// Don't fail if a cgroup hierarchy was not found, just skip this subsystem
if err == cgroups.ErrNotFound {
continue
}
return nil, err
}
paths[sysname] = subsystemPath
}
return paths, nil
}
func (c *systemdCgroup) Cleanup() error { func (c *systemdCgroup) Cleanup() error {
// systemd cleans up, we don't need to do much // systemd cleans up, we don't need to do much
paths, err := c.Paths()
if err != nil {
return err
}
for _, path := range c.cleanupDirs { for _, path := range paths {
os.RemoveAll(path) os.RemoveAll(path)
} }
return nil return nil
} }
func joinFreezer(c *cgroups.Cgroup, pid int) (string, error) { func joinFreezer(c *cgroups.Cgroup, pid int) error {
path, err := getSubsystemPath(c, "freezer") path, err := getSubsystemPath(c, "freezer")
if err != nil { if err != nil {
return "", err return err
} }
if err := os.MkdirAll(path, 0755); err != nil && !os.IsExist(err) { if err := os.MkdirAll(path, 0755); err != nil && !os.IsExist(err) {
return "", err return err
} }
if err := ioutil.WriteFile(filepath.Join(path, "cgroup.procs"), []byte(strconv.Itoa(pid)), 0700); err != nil { return ioutil.WriteFile(filepath.Join(path, "cgroup.procs"), []byte(strconv.Itoa(pid)), 0700)
return "", err
}
return path, nil
} }
func getSubsystemPath(c *cgroups.Cgroup, subsystem string) (string, error) { func getSubsystemPath(c *cgroups.Cgroup, subsystem string) (string, error) {
@ -389,20 +248,12 @@ func Freeze(c *cgroups.Cgroup, state cgroups.FreezerState) error {
} }
func GetPids(c *cgroups.Cgroup) ([]int, error) { func GetPids(c *cgroups.Cgroup) ([]int, error) {
unitName := getUnitName(c) path, err := getSubsystemPath(c, "cpu")
mountpoint, err := cgroups.FindCgroupMountpoint("cpu")
if err != nil { if err != nil {
return nil, err return nil, err
} }
props, err := theConn.GetUnitTypeProperties(unitName, getIfaceForUnit(unitName)) return cgroups.ReadProcsFile(path)
if err != nil {
return nil, err
}
cgroup := props["ControlGroup"].(string)
return cgroups.ReadProcsFile(filepath.Join(mountpoint, cgroup))
} }
func getUnitName(c *cgroups.Cgroup) string { func getUnitName(c *cgroups.Cgroup) string {
@ -437,3 +288,71 @@ func GetStats(c *cgroups.Cgroup) (*cgroups.Stats, error) {
return stats, nil return stats, nil
} }
// Atm we can't use the systemd device support because of two missing things:
// * Support for wildcards to allow mknod on any device
// * Support for wildcards to allow /dev/pts support
//
// The second is available in more recent systemd as "char-pts", but not in e.g. v208 which is
// in wide use. When both these are availalable we will be able to switch, but need to keep the old
// implementation for backwards compat.
//
// Note: we can't use systemd to set up the initial limits, and then change the cgroup
// because systemd will re-write the device settings if it needs to re-apply the cgroup context.
// This happens at least for v208 when any sibling unit is started.
func joinDevices(c *cgroups.Cgroup, pid int) error {
path, err := getSubsystemPath(c, "devices")
if err != nil {
return err
}
if err := os.MkdirAll(path, 0755); err != nil && !os.IsExist(err) {
return err
}
if err := ioutil.WriteFile(filepath.Join(path, "cgroup.procs"), []byte(strconv.Itoa(pid)), 0700); err != nil {
return err
}
if err := writeFile(path, "devices.deny", "a"); err != nil {
return err
}
for _, dev := range c.AllowedDevices {
if err := writeFile(path, "devices.allow", dev.GetCgroupAllowString()); err != nil {
return err
}
}
return nil
}
func joinMemory(c *cgroups.Cgroup, pid int) error {
memorySwap := c.MemorySwap
if memorySwap == 0 {
// By default, MemorySwap is set to twice the size of RAM.
memorySwap = c.Memory * 2
}
path, err := getSubsystemPath(c, "memory")
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(path, "memory.memsw.limit_in_bytes"), []byte(strconv.FormatInt(memorySwap, 10)), 0700)
}
// systemd does not atm set up the cpuset controller, so we must manually
// join it. Additionally that is a very finicky controller where each
// level must have a full setup as the default for a new directory is "no cpus"
func joinCpuset(c *cgroups.Cgroup, pid int) error {
path, err := getSubsystemPath(c, "cpuset")
if err != nil {
return err
}
s := &fs.CpusetGroup{}
return s.SetDir(path, c.CpusetCpus, pid)
}

View File

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -166,3 +167,23 @@ func parseCgroupFile(subsystem string, r io.Reader) (string, error) {
} }
return "", ErrNotFound return "", ErrNotFound
} }
func pathExists(path string) bool {
if _, err := os.Stat(path); err != nil {
return false
}
return true
}
func EnterPid(cgroupPaths map[string]string, pid int) error {
for _, path := range cgroupPaths {
if pathExists(path) {
if err := ioutil.WriteFile(filepath.Join(path, "cgroup.procs"),
[]byte(strconv.Itoa(pid)), 0700); err != nil {
return err
}
}
}
return nil
}

View File

@ -114,7 +114,7 @@ func OpenPtmx() (*os.File, error) {
func OpenTerminal(name string, flag int) (*os.File, error) { func OpenTerminal(name string, flag int) (*os.File, error) {
r, e := syscall.Open(name, flag, 0) r, e := syscall.Open(name, flag, 0)
if e != nil { if e != nil {
return nil, &os.PathError{"open", name, e} return nil, &os.PathError{Op: "open", Path: name, Err: e}
} }
return os.NewFile(uintptr(r), name), nil return os.NewFile(uintptr(r), name), nil
} }

View File

@ -2,6 +2,13 @@
package label package label
// InitLabels returns the process label and file labels to be used within
// the container. A list of options can be passed into this function to alter
// the labels.
func InitLabels(options []string) (string, string, error) {
return "", "", nil
}
func GenLabels(options string) (string, string, error) { func GenLabels(options string) (string, string, error) {
return "", "", nil return "", "", nil
} }
@ -22,7 +29,7 @@ func Relabel(path string, fileLabel string, relabel string) error {
return nil return nil
} }
func GetPidCon(pid int) (string, error) { func GetPidLabel(pid int) (string, error) {
return "", nil return "", nil
} }

View File

@ -9,30 +9,49 @@ import (
"github.com/docker/libcontainer/selinux" "github.com/docker/libcontainer/selinux"
) )
func GenLabels(options string) (string, string, error) { // InitLabels returns the process label and file labels to be used within
// the container. A list of options can be passed into this function to alter
// the labels. The labels returned will include a random MCS String, that is
// guaranteed to be unique.
func InitLabels(options []string) (string, string, error) {
if !selinux.SelinuxEnabled() { if !selinux.SelinuxEnabled() {
return "", "", nil return "", "", nil
} }
var err error var err error
processLabel, mountLabel := selinux.GetLxcContexts() processLabel, mountLabel := selinux.GetLxcContexts()
if processLabel != "" { if processLabel != "" {
var ( pcon := selinux.NewContext(processLabel)
s = strings.Fields(options) mcon := selinux.NewContext(mountLabel)
l = len(s) for _, opt := range options {
) if opt == "disable" {
if l > 0 { return "", "", nil
pcon := selinux.NewContext(processLabel) }
for i := 0; i < l; i++ { if i := strings.Index(opt, ":"); i == -1 {
o := strings.Split(s[i], "=") return "", "", fmt.Errorf("Bad SELinux Option")
pcon[o[0]] = o[1] }
con := strings.SplitN(opt, ":", 2)
pcon[con[0]] = con[1]
if con[0] == "level" || con[0] == "user" {
mcon[con[0]] = con[1]
} }
processLabel = pcon.Get()
mountLabel, err = selinux.CopyLevel(processLabel, mountLabel)
} }
processLabel = pcon.Get()
mountLabel = mcon.Get()
} }
return processLabel, mountLabel, err return processLabel, mountLabel, err
} }
// DEPRECATED: The GenLabels function is only to be used during the transition to the official API.
func GenLabels(options string) (string, string, error) {
return InitLabels(strings.Fields(options))
}
// FormatMountLabel returns a string to be used by the mount command.
// The format of this string will be used to alter the labeling of the mountpoint.
// The string returned is suitable to be used as the options field of the mount command.
// If you need to have additional mount point options, you can pass them in as
// the first parameter. Second parameter is the label that you wish to apply
// to all content in the mount point.
func FormatMountLabel(src, mountLabel string) string { func FormatMountLabel(src, mountLabel string) string {
if mountLabel != "" { if mountLabel != "" {
switch src { switch src {
@ -45,6 +64,8 @@ func FormatMountLabel(src, mountLabel string) string {
return src return src
} }
// SetProcessLabel takes a process label and tells the kernel to assign the
// label to the next program executed by the current process.
func SetProcessLabel(processLabel string) error { func SetProcessLabel(processLabel string) error {
if selinux.SelinuxEnabled() { if selinux.SelinuxEnabled() {
return selinux.Setexeccon(processLabel) return selinux.Setexeccon(processLabel)
@ -52,6 +73,9 @@ func SetProcessLabel(processLabel string) error {
return nil return nil
} }
// GetProcessLabel returns the process label that the kernel will assign
// to the next program executed by the current process. If "" is returned
// this indicates that the default labeling will happen for the process.
func GetProcessLabel() (string, error) { func GetProcessLabel() (string, error) {
if selinux.SelinuxEnabled() { if selinux.SelinuxEnabled() {
return selinux.Getexeccon() return selinux.Getexeccon()
@ -59,6 +83,7 @@ func GetProcessLabel() (string, error) {
return "", nil return "", nil
} }
// SetFileLabel modifies the "path" label to the specified file label
func SetFileLabel(path string, fileLabel string) error { func SetFileLabel(path string, fileLabel string) error {
if selinux.SelinuxEnabled() && fileLabel != "" { if selinux.SelinuxEnabled() && fileLabel != "" {
return selinux.Setfilecon(path, fileLabel) return selinux.Setfilecon(path, fileLabel)
@ -83,17 +108,22 @@ func Relabel(path string, fileLabel string, relabel string) error {
return selinux.Chcon(path, fileLabel, true) return selinux.Chcon(path, fileLabel, true)
} }
func GetPidCon(pid int) (string, error) { // GetPidLabel will return the label of the process running with the specified pid
func GetPidLabel(pid int) (string, error) {
if !selinux.SelinuxEnabled() { if !selinux.SelinuxEnabled() {
return "", nil return "", nil
} }
return selinux.Getpidcon(pid) return selinux.Getpidcon(pid)
} }
// Init initialises the labeling system
func Init() { func Init() {
selinux.SelinuxEnabled() selinux.SelinuxEnabled()
} }
// ReserveLabel will record the fact that the MCS label has already been used.
// This will prevent InitLabels from using the MCS label in a newly created
// container
func ReserveLabel(label string) error { func ReserveLabel(label string) error {
selinux.ReserveLabel(label) selinux.ReserveLabel(label)
return nil return nil

View File

@ -0,0 +1,48 @@
// +build selinux,linux
package label
import (
"testing"
"github.com/docker/libcontainer/selinux"
)
func TestInit(t *testing.T) {
if selinux.SelinuxEnabled() {
var testNull []string
plabel, mlabel, err := InitLabels(testNull)
if err != nil {
t.Log("InitLabels Failed")
t.Fatal(err)
}
testDisabled := []string{"disable"}
plabel, mlabel, err = InitLabels(testDisabled)
if err != nil {
t.Log("InitLabels Disabled Failed")
t.Fatal(err)
}
if plabel != "" {
t.Log("InitLabels Disabled Failed")
t.Fatal()
}
testUser := []string{"user:user_u", "role:user_r", "type:user_t", "level:s0:c1,c15"}
plabel, mlabel, err = InitLabels(testUser)
if err != nil {
t.Log("InitLabels User Failed")
t.Fatal(err)
}
if plabel != "user_u:user_r:user_t:s0:c1,c15" || mlabel != "user_u:object_r:svirt_sandbox_file_t:s0:c1,c15" {
t.Log("InitLabels User Failed")
t.Log(plabel, mlabel)
t.Fatal(err)
}
testBadData := []string{"user", "role:user_r", "type:user_t", "level:s0:c1,c15"}
plabel, mlabel, err = InitLabels(testBadData)
if err == nil {
t.Log("InitLabels Bad Failed")
t.Fatal(err)
}
}
}

View File

@ -236,7 +236,7 @@ func reOpenDevNull(rootfs string) error {
if stat.Rdev == devNullStat.Rdev { if stat.Rdev == devNullStat.Rdev {
// Close and re-open the fd. // Close and re-open the fd.
if err = syscall.Dup2(int(file.Fd()), fd); err != nil { if err = syscall.Dup2(int(file.Fd()), fd); err != nil {
return fmt.Errorf("Failed to dup fd %d to fd %d - %s", file.Fd(), fd) return fmt.Errorf("Failed to dup fd %d to fd %d - %s", file.Fd(), fd, err)
} }
} }
} }

View File

@ -56,14 +56,19 @@ func Exec(container *libcontainer.Config, stdin io.Reader, stdout, stderr io.Wri
// Do this before syncing with child so that no children // Do this before syncing with child so that no children
// can escape the cgroup // can escape the cgroup
cleaner, err := SetupCgroups(container, command.Process.Pid) cgroupRef, err := SetupCgroups(container, command.Process.Pid)
if err != nil { if err != nil {
command.Process.Kill() command.Process.Kill()
command.Wait() command.Wait()
return -1, err return -1, err
} }
if cleaner != nil { defer cgroupRef.Cleanup()
defer cleaner.Cleanup()
cgroupPaths, err := cgroupRef.Paths()
if err != nil {
command.Process.Kill()
command.Wait()
return -1, err
} }
var networkState network.NetworkState var networkState network.NetworkState
@ -77,6 +82,7 @@ func Exec(container *libcontainer.Config, stdin io.Reader, stdout, stderr io.Wri
InitPid: command.Process.Pid, InitPid: command.Process.Pid,
InitStartTime: started, InitStartTime: started,
NetworkState: networkState, NetworkState: networkState,
CgroupPaths: cgroupPaths,
} }
if err := libcontainer.SaveState(dataPath, state); err != nil { if err := libcontainer.SaveState(dataPath, state); err != nil {
@ -133,7 +139,7 @@ func DefaultCreateCommand(container *libcontainer.Config, console, rootfs, dataP
} }
*/ */
command := exec.Command(init, append([]string{"init"}, args...)...) command := exec.Command(init, append([]string{"init", "--"}, args...)...)
// make sure the process is executed inside the context of the rootfs // make sure the process is executed inside the context of the rootfs
command.Dir = rootfs command.Dir = rootfs
command.Env = append(os.Environ(), env...) command.Env = append(os.Environ(), env...)

View File

@ -3,26 +3,49 @@
package namespaces package namespaces
import ( import (
"encoding/json" "fmt"
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/label"
"github.com/docker/libcontainer/system"
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strconv" "strconv"
"syscall" "syscall"
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/cgroups"
"github.com/docker/libcontainer/label"
"github.com/docker/libcontainer/syncpipe"
"github.com/docker/libcontainer/system"
) )
// Runs the command under 'args' inside an existing container referred to by 'container'. // ExecIn reexec's the initPath with the argv 0 rewrite to "nsenter" so that it is able to run the
// Returns the exitcode of the command upon success and appropriate error on failure. // setns code in a single threaded environment joining the existing containers' namespaces.
func RunIn(container *libcontainer.Config, state *libcontainer.State, args []string, nsinitPath string, stdin io.Reader, stdout, stderr io.Writer, console string, startCallback func(*exec.Cmd)) (int, error) { func ExecIn(container *libcontainer.Config, state *libcontainer.State, userArgs []string, initPath, action string,
initArgs, err := getNsEnterCommand(strconv.Itoa(state.InitPid), container, console, args) stdin io.Reader, stdout, stderr io.Writer, console string, startCallback func(*exec.Cmd)) (int, error) {
args := []string{fmt.Sprintf("nsenter-%s", action), "--nspid", strconv.Itoa(state.InitPid)}
if console != "" {
args = append(args, "--console", console)
}
cmd := &exec.Cmd{
Path: initPath,
Args: append(args, append([]string{"--"}, userArgs...)...),
}
if filepath.Base(initPath) == initPath {
if lp, err := exec.LookPath(initPath); err == nil {
cmd.Path = lp
}
}
pipe, err := syncpipe.NewSyncPipe()
if err != nil { if err != nil {
return -1, err return -1, err
} }
defer pipe.Close()
cmd := exec.Command(nsinitPath, initArgs...)
// Note: these are only used in non-tty mode // Note: these are only used in non-tty mode
// if there is a tty for the container it will be opened within the namespace and the // if there is a tty for the container it will be opened within the namespace and the
// fds will be duped to stdin, stdiout, and stderr // fds will be duped to stdin, stdiout, and stderr
@ -30,9 +53,24 @@ func RunIn(container *libcontainer.Config, state *libcontainer.State, args []str
cmd.Stdout = stdout cmd.Stdout = stdout
cmd.Stderr = stderr cmd.Stderr = stderr
cmd.ExtraFiles = []*os.File{pipe.Child()}
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return -1, err return -1, err
} }
pipe.CloseChild()
// Enter cgroups.
if err := EnterCgroups(state, cmd.Process.Pid); err != nil {
return -1, err
}
if err := pipe.SendToChild(container); err != nil {
cmd.Process.Kill()
cmd.Wait()
return -1, err
}
if startCallback != nil { if startCallback != nil {
startCallback(cmd) startCallback(cmd)
} }
@ -46,61 +84,14 @@ func RunIn(container *libcontainer.Config, state *libcontainer.State, args []str
return cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus(), nil return cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus(), nil
} }
// ExecIn uses an existing pid and joins the pid's namespaces with the new command. // Finalize expects that the setns calls have been setup and that is has joined an
func ExecIn(container *libcontainer.Config, state *libcontainer.State, args []string) error { // existing namespace
// Enter the namespace and then finish setup func FinalizeSetns(container *libcontainer.Config, args []string) error {
args, err := getNsEnterCommand(strconv.Itoa(state.InitPid), container, "", args) // clear the current processes env and replace it with the environment defined on the container
if err != nil {
return err
}
finalArgs := append([]string{os.Args[0]}, args...)
if err := system.Execv(finalArgs[0], finalArgs[0:], os.Environ()); err != nil {
return err
}
panic("unreachable")
}
func getContainerJson(container *libcontainer.Config) (string, error) {
// TODO(vmarmol): If this gets too long, send it over a pipe to the child.
// Marshall the container into JSON since it won't be available in the namespace.
containerJson, err := json.Marshal(container)
if err != nil {
return "", err
}
return string(containerJson), nil
}
func getNsEnterCommand(initPid string, container *libcontainer.Config, console string, args []string) ([]string, error) {
containerJson, err := getContainerJson(container)
if err != nil {
return nil, err
}
out := []string{
"nsenter",
"--nspid", initPid,
"--containerjson", containerJson,
}
if console != "" {
out = append(out, "--console", console)
}
out = append(out, "--")
out = append(out, args...)
return out, nil
}
// Run a command in a container after entering the namespace.
func NsEnter(container *libcontainer.Config, args []string) error {
// clear the current processes env and replace it with the environment
// defined on the container
if err := LoadContainerEnvironment(container); err != nil { if err := LoadContainerEnvironment(container); err != nil {
return err return err
} }
if err := FinalizeNamespace(container); err != nil { if err := FinalizeNamespace(container); err != nil {
return err return err
} }
@ -114,5 +105,10 @@ func NsEnter(container *libcontainer.Config, args []string) error {
if err := system.Execv(args[0], args[0:], container.Env); err != nil { if err := system.Execv(args[0], args[0:], container.Env); err != nil {
return err return err
} }
panic("unreachable") panic("unreachable")
} }
func EnterCgroups(state *libcontainer.State, pid int) error {
return cgroups.EnterPid(state.CgroupPaths, pid)
}

View File

@ -5,11 +5,9 @@ package namespaces
import ( import (
"fmt" "fmt"
"os" "os"
"runtime"
"strings" "strings"
"syscall" "syscall"
"github.com/docker/docker/pkg/user"
"github.com/docker/libcontainer" "github.com/docker/libcontainer"
"github.com/docker/libcontainer/apparmor" "github.com/docker/libcontainer/apparmor"
"github.com/docker/libcontainer/console" "github.com/docker/libcontainer/console"
@ -21,6 +19,7 @@ import (
"github.com/docker/libcontainer/security/restrict" "github.com/docker/libcontainer/security/restrict"
"github.com/docker/libcontainer/syncpipe" "github.com/docker/libcontainer/syncpipe"
"github.com/docker/libcontainer/system" "github.com/docker/libcontainer/system"
"github.com/docker/libcontainer/user"
"github.com/docker/libcontainer/utils" "github.com/docker/libcontainer/utils"
) )
@ -28,6 +27,8 @@ import (
// Move this to libcontainer package. // Move this to libcontainer package.
// Init is the init process that first runs inside a new namespace to setup mounts, users, networking, // Init is the init process that first runs inside a new namespace to setup mounts, users, networking,
// and other options required for the new container. // and other options required for the new container.
// The caller of Init function has to ensure that the go runtime is locked to an OS thread
// (using runtime.LockOSThread) else system calls like setns called within Init may not work as intended.
func Init(container *libcontainer.Config, uncleanRootfs, consolePath string, syncPipe *syncpipe.SyncPipe, args []string) (err error) { func Init(container *libcontainer.Config, uncleanRootfs, consolePath string, syncPipe *syncpipe.SyncPipe, args []string) (err error) {
defer func() { defer func() {
if err != nil { if err != nil {
@ -47,8 +48,8 @@ func Init(container *libcontainer.Config, uncleanRootfs, consolePath string, syn
} }
// We always read this as it is a way to sync with the parent as well // We always read this as it is a way to sync with the parent as well
networkState, err := syncPipe.ReadFromParent() var networkState *network.NetworkState
if err != nil { if err := syncPipe.ReadFromParent(&networkState); err != nil {
return err return err
} }
@ -87,8 +88,6 @@ func Init(container *libcontainer.Config, uncleanRootfs, consolePath string, syn
} }
} }
runtime.LockOSThread()
if err := apparmor.ApplyProfile(container.AppArmorProfile); err != nil { if err := apparmor.ApplyProfile(container.AppArmorProfile); err != nil {
return fmt.Errorf("set apparmor profile %s: %s", container.AppArmorProfile, err) return fmt.Errorf("set apparmor profile %s: %s", container.AppArmorProfile, err)
} }
@ -119,7 +118,7 @@ func Init(container *libcontainer.Config, uncleanRootfs, consolePath string, syn
return fmt.Errorf("restore parent death signal %s", err) return fmt.Errorf("restore parent death signal %s", err)
} }
return system.Execv(args[0], args[0:], container.Env) return system.Execv(args[0], args[0:], os.Environ())
} }
// RestoreParentDeathSignal sets the parent death signal to old. // RestoreParentDeathSignal sets the parent death signal to old.
@ -152,7 +151,7 @@ func RestoreParentDeathSignal(old int) error {
// SetupUser changes the groups, gid, and uid for the user inside the container // SetupUser changes the groups, gid, and uid for the user inside the container
func SetupUser(u string) error { func SetupUser(u string) error {
uid, gid, suppGids, err := user.GetUserGroupSupplementary(u, syscall.Getuid(), syscall.Getgid()) uid, gid, suppGids, home, err := user.GetUserGroupSupplementaryHome(u, syscall.Getuid(), syscall.Getgid(), "/")
if err != nil { if err != nil {
return fmt.Errorf("get supplementary groups %s", err) return fmt.Errorf("get supplementary groups %s", err)
} }
@ -169,6 +168,13 @@ func SetupUser(u string) error {
return fmt.Errorf("setuid %s", err) return fmt.Errorf("setuid %s", err)
} }
// if we didn't get HOME already, set it based on the user's HOME
if envHome := os.Getenv("HOME"); envHome == "" {
if err := os.Setenv("HOME", home); err != nil {
return fmt.Errorf("set HOME %s", err)
}
}
return nil return nil
} }

View File

@ -1,223 +0,0 @@
// +build linux
package namespaces
/*
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <linux/sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <getopt.h>
static const kBufSize = 256;
void get_args(int *argc, char ***argv) {
// Read argv
int fd = open("/proc/self/cmdline", O_RDONLY);
// Read the whole commandline.
ssize_t contents_size = 0;
ssize_t contents_offset = 0;
char *contents = NULL;
ssize_t bytes_read = 0;
do {
contents_size += kBufSize;
contents = (char *) realloc(contents, contents_size);
bytes_read = read(fd, contents + contents_offset, contents_size - contents_offset);
contents_offset += bytes_read;
} while (bytes_read > 0);
close(fd);
// Parse the commandline into an argv. /proc/self/cmdline has \0 delimited args.
ssize_t i;
*argc = 0;
for (i = 0; i < contents_offset; i++) {
if (contents[i] == '\0') {
(*argc)++;
}
}
*argv = (char **) malloc(sizeof(char *) * ((*argc) + 1));
int idx;
for (idx = 0; idx < (*argc); idx++) {
(*argv)[idx] = contents;
contents += strlen(contents) + 1;
}
(*argv)[*argc] = NULL;
}
// Use raw setns syscall for versions of glibc that don't include it (namely glibc-2.12)
#if __GLIBC__ == 2 && __GLIBC_MINOR__ < 14
#define _GNU_SOURCE
#include <sched.h>
#include "syscall.h"
#ifdef SYS_setns
int setns(int fd, int nstype) {
return syscall(SYS_setns, fd, nstype);
}
#endif
#endif
void print_usage() {
fprintf(stderr, "<binary> nsenter --nspid <pid> --containerjson <container_json> -- cmd1 arg1 arg2...\n");
}
void nsenter() {
int argc;
char **argv;
get_args(&argc, &argv);
// Ignore if this is not for us.
if (argc < 2 || strcmp(argv[1], "nsenter") != 0) {
return;
}
// USAGE: <binary> nsenter <PID> <process label> <container JSON> <argv>...
if (argc < 6) {
fprintf(stderr, "nsenter: Incorrect usage, not enough arguments\n");
exit(1);
}
static const struct option longopts[] = {
{ "nspid", required_argument, NULL, 'n' },
{ "containerjson", required_argument, NULL, 'c' },
{ "console", required_argument, NULL, 't' },
{ NULL, 0, NULL, 0 }
};
int c;
pid_t init_pid = -1;
char *init_pid_str = NULL;
char *container_json = NULL;
char *console = NULL;
while ((c = getopt_long_only(argc, argv, "n:s:c:", longopts, NULL)) != -1) {
switch (c) {
case 'n':
init_pid_str = optarg;
break;
case 'c':
container_json = optarg;
break;
case 't':
console = optarg;
break;
}
}
if (container_json == NULL || init_pid_str == NULL) {
print_usage();
exit(1);
}
init_pid = strtol(init_pid_str, NULL, 10);
if (errno != 0 || init_pid <= 0) {
fprintf(stderr, "nsenter: Failed to parse PID from \"%s\" with error: \"%s\"\n", init_pid_str, strerror(errno));
print_usage();
exit(1);
}
argc -= 3;
argv += 3;
if (setsid() == -1) {
fprintf(stderr, "setsid failed. Error: %s\n", strerror(errno));
exit(1);
}
// before we setns we need to dup the console
int consolefd = -1;
if (console != NULL) {
consolefd = open(console, O_RDWR);
if (consolefd < 0) {
fprintf(stderr, "nsenter: failed to open console %s %s\n", console, strerror(errno));
exit(1);
}
}
// Setns on all supported namespaces.
char ns_dir[PATH_MAX];
memset(ns_dir, 0, PATH_MAX);
snprintf(ns_dir, PATH_MAX - 1, "/proc/%d/ns/", init_pid);
struct dirent *dent;
DIR *dir = opendir(ns_dir);
if (dir == NULL) {
fprintf(stderr, "nsenter: Failed to open directory \"%s\" with error: \"%s\"\n", ns_dir, strerror(errno));
exit(1);
}
while((dent = readdir(dir)) != NULL) {
if(strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0 || strcmp(dent->d_name, "user") == 0) {
continue;
}
// Get and open the namespace for the init we are joining..
char buf[PATH_MAX];
memset(buf, 0, PATH_MAX);
snprintf(buf, PATH_MAX - 1, "%s%s", ns_dir, dent->d_name);
int fd = open(buf, O_RDONLY);
if (fd == -1) {
fprintf(stderr, "nsenter: Failed to open ns file \"%s\" for ns \"%s\" with error: \"%s\"\n", buf, dent->d_name, strerror(errno));
exit(1);
}
// Set the namespace.
if (setns(fd, 0) == -1) {
fprintf(stderr, "nsenter: Failed to setns for \"%s\" with error: \"%s\"\n", dent->d_name, strerror(errno));
exit(1);
}
close(fd);
}
closedir(dir);
// We must fork to actually enter the PID namespace.
int child = fork();
if (child == 0) {
if (consolefd != -1) {
if (dup2(consolefd, STDIN_FILENO) != 0) {
fprintf(stderr, "nsenter: failed to dup 0 %s\n", strerror(errno));
exit(1);
}
if (dup2(consolefd, STDOUT_FILENO) != STDOUT_FILENO) {
fprintf(stderr, "nsenter: failed to dup 1 %s\n", strerror(errno));
exit(1);
}
if (dup2(consolefd, STDERR_FILENO) != STDERR_FILENO) {
fprintf(stderr, "nsenter: failed to dup 2 %s\n", strerror(errno));
exit(1);
}
}
// Finish executing, let the Go runtime take over.
return;
} else {
// Parent, wait for the child.
int status = 0;
if (waitpid(child, &status, 0) == -1) {
fprintf(stderr, "nsenter: Failed to waitpid with error: \"%s\"\n", strerror(errno));
exit(1);
}
// Forward the child's exit code or re-send its death signal.
if (WIFEXITED(status)) {
exit(WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
kill(getpid(), WTERMSIG(status));
}
exit(1);
}
return;
}
__attribute__((constructor)) init() {
nsenter();
}
*/
import "C"

View File

@ -0,0 +1,6 @@
## nsenter
The `nsenter` package registers a special init constructor that is called before the Go runtime has
a chance to boot. This provides us the ability to `setns` on existing namespaces and avoid the issues
that the Go runtime has with multiple threads. This constructor is only called if this package is
registered, imported, in your go application and the argv 0 is `nsenter`.

View File

@ -0,0 +1,218 @@
// +build cgo
//
// formated with indent -linux nsenter.c
#include <errno.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <linux/sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <getopt.h>
static const kBufSize = 256;
static const char *kNsEnter = "nsenter";
void get_args(int *argc, char ***argv)
{
// Read argv
int fd = open("/proc/self/cmdline", O_RDONLY);
// Read the whole commandline.
ssize_t contents_size = 0;
ssize_t contents_offset = 0;
char *contents = NULL;
ssize_t bytes_read = 0;
do {
contents_size += kBufSize;
contents = (char *)realloc(contents, contents_size);
bytes_read =
read(fd, contents + contents_offset,
contents_size - contents_offset);
contents_offset += bytes_read;
}
while (bytes_read > 0);
close(fd);
// Parse the commandline into an argv. /proc/self/cmdline has \0 delimited args.
ssize_t i;
*argc = 0;
for (i = 0; i < contents_offset; i++) {
if (contents[i] == '\0') {
(*argc)++;
}
}
*argv = (char **)malloc(sizeof(char *) * ((*argc) + 1));
int idx;
for (idx = 0; idx < (*argc); idx++) {
(*argv)[idx] = contents;
contents += strlen(contents) + 1;
}
(*argv)[*argc] = NULL;
}
// Use raw setns syscall for versions of glibc that don't include it (namely glibc-2.12)
#if __GLIBC__ == 2 && __GLIBC_MINOR__ < 14
#define _GNU_SOURCE
#include <sched.h>
#include "syscall.h"
#ifdef SYS_setns
int setns(int fd, int nstype)
{
return syscall(SYS_setns, fd, nstype);
}
#endif
#endif
void print_usage()
{
fprintf(stderr,
"nsenter --nspid <pid> --console <console> -- cmd1 arg1 arg2...\n");
}
void nsenter()
{
int argc, c;
char **argv;
get_args(&argc, &argv);
// check argv 0 to ensure that we are supposed to setns
// we use strncmp to test for a value of "nsenter" but also allows alternate implmentations
// after the setns code path to continue to use the argv 0 to determine actions to be run
// resulting in the ability to specify "nsenter-mknod", "nsenter-exec", etc...
if (strncmp(argv[0], kNsEnter, strlen(kNsEnter)) != 0) {
return;
}
static const struct option longopts[] = {
{"nspid", required_argument, NULL, 'n'},
{"console", required_argument, NULL, 't'},
{NULL, 0, NULL, 0}
};
pid_t init_pid = -1;
char *init_pid_str = NULL;
char *console = NULL;
while ((c = getopt_long_only(argc, argv, "n:c:", longopts, NULL)) != -1) {
switch (c) {
case 'n':
init_pid_str = optarg;
break;
case 't':
console = optarg;
break;
}
}
if (init_pid_str == NULL) {
print_usage();
exit(1);
}
init_pid = strtol(init_pid_str, NULL, 10);
if ((init_pid == 0 && errno == EINVAL) || errno == ERANGE) {
fprintf(stderr,
"nsenter: Failed to parse PID from \"%s\" with output \"%d\" and error: \"%s\"\n",
init_pid_str, init_pid, strerror(errno));
print_usage();
exit(1);
}
argc -= 3;
argv += 3;
if (setsid() == -1) {
fprintf(stderr, "setsid failed. Error: %s\n", strerror(errno));
exit(1);
}
// before we setns we need to dup the console
int consolefd = -1;
if (console != NULL) {
consolefd = open(console, O_RDWR);
if (consolefd < 0) {
fprintf(stderr,
"nsenter: failed to open console %s %s\n",
console, strerror(errno));
exit(1);
}
}
// Setns on all supported namespaces.
char ns_dir[PATH_MAX];
memset(ns_dir, 0, PATH_MAX);
snprintf(ns_dir, PATH_MAX - 1, "/proc/%d/ns/", init_pid);
char *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" };
const int num = sizeof(namespaces) / sizeof(char *);
int i;
for (i = 0; i < num; i++) {
char buf[PATH_MAX];
memset(buf, 0, PATH_MAX);
snprintf(buf, PATH_MAX - 1, "%s%s", ns_dir, namespaces[i]);
int fd = open(buf, O_RDONLY);
if (fd == -1) {
// Ignore nonexistent namespaces.
if (errno == ENOENT)
continue;
fprintf(stderr,
"nsenter: Failed to open ns file \"%s\" for ns \"%s\" with error: \"%s\"\n",
buf, namespaces[i], strerror(errno));
exit(1);
}
// Set the namespace.
if (setns(fd, 0) == -1) {
fprintf(stderr,
"nsenter: Failed to setns for \"%s\" with error: \"%s\"\n",
namespaces[i], strerror(errno));
exit(1);
}
close(fd);
}
// We must fork to actually enter the PID namespace.
int child = fork();
if (child == 0) {
if (consolefd != -1) {
if (dup2(consolefd, STDIN_FILENO) != 0) {
fprintf(stderr, "nsenter: failed to dup 0 %s\n",
strerror(errno));
exit(1);
}
if (dup2(consolefd, STDOUT_FILENO) != STDOUT_FILENO) {
fprintf(stderr, "nsenter: failed to dup 1 %s\n",
strerror(errno));
exit(1);
}
if (dup2(consolefd, STDERR_FILENO) != STDERR_FILENO) {
fprintf(stderr, "nsenter: failed to dup 2 %s\n",
strerror(errno));
exit(1);
}
}
// Finish executing, let the Go runtime take over.
return;
} else {
// Parent, wait for the child.
int status = 0;
if (waitpid(child, &status, 0) == -1) {
fprintf(stderr,
"nsenter: Failed to waitpid with error: \"%s\"\n",
strerror(errno));
exit(1);
}
// Forward the child's exit code or re-send its death signal.
if (WIFEXITED(status)) {
exit(WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
kill(getpid(), WTERMSIG(status));
}
exit(1);
}
return;
}

View File

@ -0,0 +1,10 @@
// +build linux
package nsenter
/*
__attribute__((constructor)) init() {
nsenter();
}
*/
import "C"

View File

@ -0,0 +1,3 @@
// +build !linux !cgo
package nsenter

View File

@ -21,3 +21,10 @@ type Route struct {
Iface *net.Interface Iface *net.Interface
Default bool Default bool
} }
// An IfAddr defines IP network settings for a given network interface
type IfAddr struct {
Iface *net.Interface
IP net.IP
IPNet *net.IPNet
}

Binary file not shown.

View File

@ -189,13 +189,15 @@ func newRtAttrChild(parent *RtAttr, attrType int, data []byte) *RtAttr {
} }
func (a *RtAttr) Len() int { func (a *RtAttr) Len() int {
if len(a.children) == 0 {
return (syscall.SizeofRtAttr + len(a.Data))
}
l := 0 l := 0
for _, child := range a.children { for _, child := range a.children {
l += child.Len() + syscall.SizeofRtAttr l += child.Len()
}
if l == 0 {
l++
} }
l += syscall.SizeofRtAttr
return rtaAlignOf(l + len(a.Data)) return rtaAlignOf(l + len(a.Data))
} }
@ -203,7 +205,7 @@ func (a *RtAttr) ToWireFormat() []byte {
native := nativeEndian() native := nativeEndian()
length := a.Len() length := a.Len()
buf := make([]byte, rtaAlignOf(length+syscall.SizeofRtAttr)) buf := make([]byte, rtaAlignOf(length))
if a.Data != nil { if a.Data != nil {
copy(buf[4:], a.Data) copy(buf[4:], a.Data)
@ -216,11 +218,10 @@ func (a *RtAttr) ToWireFormat() []byte {
} }
} }
if l := uint16(rtaAlignOf(length)); l != 0 { if l := uint16(length); l != 0 {
native.PutUint16(buf[0:2], l+1) native.PutUint16(buf[0:2], l)
} }
native.PutUint16(buf[2:4], a.Type) native.PutUint16(buf[2:4], a.Type)
return buf return buf
} }
@ -650,30 +651,28 @@ func NetworkSetNsFd(iface *net.Interface, fd int) error {
return s.HandleAck(wb.Seq) return s.HandleAck(wb.Seq)
} }
// Add an Ip address to an interface. This is identical to: func networkLinkIpAction(action, flags int, ifa IfAddr) error {
// ip addr add $ip/$ipNet dev $iface
func NetworkLinkAddIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error {
s, err := getNetlinkSocket() s, err := getNetlinkSocket()
if err != nil { if err != nil {
return err return err
} }
defer s.Close() defer s.Close()
family := getIpFamily(ip) family := getIpFamily(ifa.IP)
wb := newNetlinkRequest(syscall.RTM_NEWADDR, syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK) wb := newNetlinkRequest(action, flags)
msg := newIfAddrmsg(family) msg := newIfAddrmsg(family)
msg.Index = uint32(iface.Index) msg.Index = uint32(ifa.Iface.Index)
prefixLen, _ := ipNet.Mask.Size() prefixLen, _ := ifa.IPNet.Mask.Size()
msg.Prefixlen = uint8(prefixLen) msg.Prefixlen = uint8(prefixLen)
wb.AddData(msg) wb.AddData(msg)
var ipData []byte var ipData []byte
if family == syscall.AF_INET { if family == syscall.AF_INET {
ipData = ip.To4() ipData = ifa.IP.To4()
} else { } else {
ipData = ip.To16() ipData = ifa.IP.To16()
} }
localData := newRtAttr(syscall.IFA_LOCAL, ipData) localData := newRtAttr(syscall.IFA_LOCAL, ipData)
@ -689,6 +688,26 @@ func NetworkLinkAddIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error {
return s.HandleAck(wb.Seq) return s.HandleAck(wb.Seq)
} }
// Delete an IP address from an interface. This is identical to:
// ip addr del $ip/$ipNet dev $iface
func NetworkLinkDelIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error {
return networkLinkIpAction(
syscall.RTM_DELADDR,
syscall.NLM_F_ACK,
IfAddr{iface, ip, ipNet},
)
}
// Add an Ip address to an interface. This is identical to:
// ip addr add $ip/$ipNet dev $iface
func NetworkLinkAddIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error {
return networkLinkIpAction(
syscall.RTM_NEWADDR,
syscall.NLM_F_CREATE|syscall.NLM_F_EXCL|syscall.NLM_F_ACK,
IfAddr{iface, ip, ipNet},
)
}
func zeroTerminated(s string) []byte { func zeroTerminated(s string) []byte {
return []byte(s + "\000") return []byte(s + "\000")
} }
@ -700,6 +719,10 @@ func nonZeroTerminated(s string) []byte {
// Add a new network link of a specified type. This is identical to // Add a new network link of a specified type. This is identical to
// running: ip add link $name type $linkType // running: ip add link $name type $linkType
func NetworkLinkAdd(name string, linkType string) error { func NetworkLinkAdd(name string, linkType string) error {
if name == "" || linkType == "" {
return fmt.Errorf("Neither link name nor link type can be empty!")
}
s, err := getNetlinkSocket() s, err := getNetlinkSocket()
if err != nil { if err != nil {
return err return err
@ -711,15 +734,43 @@ func NetworkLinkAdd(name string, linkType string) error {
msg := newIfInfomsg(syscall.AF_UNSPEC) msg := newIfInfomsg(syscall.AF_UNSPEC)
wb.AddData(msg) wb.AddData(msg)
if name != "" { linkInfo := newRtAttr(syscall.IFLA_LINKINFO, nil)
nameData := newRtAttr(syscall.IFLA_IFNAME, zeroTerminated(name)) newRtAttrChild(linkInfo, IFLA_INFO_KIND, nonZeroTerminated(linkType))
wb.AddData(nameData) wb.AddData(linkInfo)
nameData := newRtAttr(syscall.IFLA_IFNAME, zeroTerminated(name))
wb.AddData(nameData)
if err := s.Send(wb); err != nil {
return err
} }
kindData := newRtAttr(IFLA_INFO_KIND, nonZeroTerminated(linkType)) return s.HandleAck(wb.Seq)
}
infoData := newRtAttr(syscall.IFLA_LINKINFO, kindData.ToWireFormat()) // Delete a network link. This is identical to
wb.AddData(infoData) // running: ip link del $name
func NetworkLinkDel(name string) error {
if name == "" {
return fmt.Errorf("Network link name can not be empty!")
}
s, err := getNetlinkSocket()
if err != nil {
return err
}
defer s.Close()
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
wb := newNetlinkRequest(syscall.RTM_DELLINK, syscall.NLM_F_ACK)
msg := newIfInfomsg(syscall.AF_UNSPEC)
msg.Index = int32(iface.Index)
wb.AddData(msg)
if err := s.Send(wb); err != nil { if err := s.Send(wb); err != nil {
return err return err

View File

@ -2,9 +2,55 @@ package netlink
import ( import (
"net" "net"
"strings"
"testing" "testing"
) )
func ipAssigned(iface *net.Interface, ip net.IP) bool {
addrs, _ := iface.Addrs()
for _, addr := range addrs {
args := strings.SplitN(addr.String(), "/", 2)
if args[0] == ip.String() {
return true
}
}
return false
}
func TestAddDelNetworkIp(t *testing.T) {
if testing.Short() {
return
}
ifaceName := "lo"
ip := net.ParseIP("127.0.1.1")
mask := net.IPv4Mask(255, 255, 255, 255)
ipNet := &net.IPNet{IP: ip, Mask: mask}
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
t.Skip("No 'lo' interface; skipping tests")
}
if err := NetworkLinkAddIp(iface, ip, ipNet); err != nil {
t.Fatal(err)
}
if !ipAssigned(iface, ip) {
t.Fatalf("Could not locate address '%s' in lo address list.", ip.String())
}
if err := NetworkLinkDelIp(iface, ip, ipNet); err != nil {
t.Fatal(err)
}
if ipAssigned(iface, ip) {
t.Fatalf("Located address '%s' in lo address list after removal.", ip.String())
}
}
func TestCreateBridgeWithMac(t *testing.T) { func TestCreateBridgeWithMac(t *testing.T) {
if testing.Short() { if testing.Short() {
return return
@ -27,10 +73,35 @@ func TestCreateBridgeWithMac(t *testing.T) {
} }
if _, err := net.InterfaceByName(name); err == nil { if _, err := net.InterfaceByName(name); err == nil {
t.Fatal("expected error getting interface because bridge was deleted") t.Fatalf("expected error getting interface because %s bridge was deleted", name)
} }
} }
func TestCreateBridgeLink(t *testing.T) {
if testing.Short() {
return
}
name := "mybrlink"
if err := NetworkLinkAdd(name, "bridge"); err != nil {
t.Fatal(err)
}
if _, err := net.InterfaceByName(name); err != nil {
t.Fatal(err)
}
if err := NetworkLinkDel(name); err != nil {
t.Fatal(err)
}
if _, err := net.InterfaceByName(name); err == nil {
t.Fatalf("expected error getting interface because %s bridge was deleted", name)
}
}
func TestCreateVethPair(t *testing.T) { func TestCreateVethPair(t *testing.T) {
if testing.Short() { if testing.Short() {
return return

View File

@ -19,6 +19,10 @@ func NetworkLinkAdd(name string, linkType string) error {
return ErrNotImplemented return ErrNotImplemented
} }
func NetworkLinkDel(name string) error {
return ErrNotImplemented
}
func NetworkLinkUp(iface *net.Interface) error { func NetworkLinkUp(iface *net.Interface) error {
return ErrNotImplemented return ErrNotImplemented
} }
@ -27,6 +31,10 @@ func NetworkLinkAddIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error {
return ErrNotImplemented return ErrNotImplemented
} }
func NetworkLinkDelIp(iface *net.Interface, ip net.IP, ipNet *net.IPNet) error {
return ErrNotImplemented
}
func AddRoute(destination, source, gateway, device string) error { func AddRoute(destination, source, gateway, device string) error {
return ErrNotImplemented return ErrNotImplemented
} }

View File

@ -44,6 +44,14 @@ func SetInterfaceInNamespacePid(name string, nsPid int) error {
return netlink.NetworkSetNsPid(iface, nsPid) return netlink.NetworkSetNsPid(iface, nsPid)
} }
func SetInterfaceInNamespaceFd(name string, fd uintptr) error {
iface, err := net.InterfaceByName(name)
if err != nil {
return err
}
return netlink.NetworkSetNsFd(iface, int(fd))
}
func SetInterfaceMaster(name, master string) error { func SetInterfaceMaster(name, master string) error {
iface, err := net.InterfaceByName(name) iface, err := net.InterfaceByName(name)
if err != nil { if err != nil {

View File

@ -1,42 +0,0 @@
package nsinit
import (
"log"
"os"
"github.com/codegangsta/cli"
)
var logPath = os.Getenv("log")
func preload(context *cli.Context) error {
if logPath != "" {
if err := openLog(logPath); err != nil {
return err
}
}
return nil
}
func NsInit() {
app := cli.NewApp()
app.Name = "nsinit"
app.Version = "0.1"
app.Author = "libcontainer maintainers"
app.Before = preload
app.Commands = []cli.Command{
execCommand,
initCommand,
statsCommand,
configCommand,
nsenterCommand,
pauseCommand,
unpauseCommand,
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}

View File

@ -1,4 +1,4 @@
package nsinit package main
import ( import (
"encoding/json" "encoding/json"
@ -15,7 +15,7 @@ var configCommand = cli.Command{
} }
func configAction(context *cli.Context) { func configAction(context *cli.Context) {
container, err := loadContainer() container, err := loadConfig()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -1,4 +1,4 @@
package nsinit package main
import ( import (
"fmt" "fmt"
@ -8,6 +8,7 @@ import (
"os/exec" "os/exec"
"os/signal" "os/signal"
"syscall" "syscall"
"text/tabwriter"
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
"github.com/docker/docker/pkg/term" "github.com/docker/docker/pkg/term"
@ -20,12 +21,29 @@ var execCommand = cli.Command{
Name: "exec", Name: "exec",
Usage: "execute a new command inside a container", Usage: "execute a new command inside a container",
Action: execAction, Action: execAction,
Flags: []cli.Flag{
cli.BoolFlag{Name: "list", Usage: "list all registered exec functions"},
cli.StringFlag{Name: "func", Value: "exec", Usage: "function name to exec inside a container"},
},
} }
func execAction(context *cli.Context) { func execAction(context *cli.Context) {
if context.Bool("list") {
w := tabwriter.NewWriter(os.Stdout, 10, 1, 3, ' ', 0)
fmt.Fprint(w, "NAME\tUSAGE\n")
for k, f := range argvs {
fmt.Fprintf(w, "%s\t%s\n", k, f.Usage)
}
w.Flush()
return
}
var exitCode int var exitCode int
container, err := loadContainer() container, err := loadConfig()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -36,7 +54,7 @@ func execAction(context *cli.Context) {
} }
if state != nil { if state != nil {
exitCode, err = runIn(container, state, []string(context.Args())) exitCode, err = startInExistingContainer(container, state, context.String("func"), context)
} else { } else {
exitCode, err = startContainer(container, dataPath, []string(context.Args())) exitCode, err = startContainer(container, dataPath, []string(context.Args()))
} }
@ -48,28 +66,32 @@ func execAction(context *cli.Context) {
os.Exit(exitCode) os.Exit(exitCode)
} }
func runIn(container *libcontainer.Config, state *libcontainer.State, args []string) (int, error) { // the process for execing a new process inside an existing container is that we have to exec ourself
// with the nsenter argument so that the C code can setns an the namespaces that we require. Then that
// code path will drop us into the path that we can do the final setup of the namespace and exec the users
// application.
func startInExistingContainer(config *libcontainer.Config, state *libcontainer.State, action string, context *cli.Context) (int, error) {
var ( var (
master *os.File master *os.File
console string console string
err error err error
sigc = make(chan os.Signal, 10)
stdin = os.Stdin stdin = os.Stdin
stdout = os.Stdout stdout = os.Stdout
stderr = os.Stderr stderr = os.Stderr
sigc = make(chan os.Signal, 10)
) )
signal.Notify(sigc) signal.Notify(sigc)
if container.Tty { if config.Tty {
stdin = nil stdin = nil
stdout = nil stdout = nil
stderr = nil stderr = nil
master, console, err = consolepkg.CreateMasterAndConsole() master, console, err = consolepkg.CreateMasterAndConsole()
if err != nil { if err != nil {
log.Fatal(err) return -1, err
} }
go io.Copy(master, os.Stdin) go io.Copy(master, os.Stdin)
@ -77,7 +99,7 @@ func runIn(container *libcontainer.Config, state *libcontainer.State, args []str
state, err := term.SetRawTerminal(os.Stdin.Fd()) state, err := term.SetRawTerminal(os.Stdin.Fd())
if err != nil { if err != nil {
log.Fatal(err) return -1, err
} }
defer term.RestoreTerminal(os.Stdin.Fd(), state) defer term.RestoreTerminal(os.Stdin.Fd(), state)
@ -98,7 +120,7 @@ func runIn(container *libcontainer.Config, state *libcontainer.State, args []str
}() }()
} }
return namespaces.RunIn(container, state, args, os.Args[0], stdin, stdout, stderr, console, startCallback) return namespaces.ExecIn(config, state, context.Args(), os.Args[0], action, stdin, stdout, stderr, console, startCallback)
} }
// startContainer starts the container. Returns the exit status or -1 and an // startContainer starts the container. Returns the exit status or -1 and an

View File

@ -1,8 +1,9 @@
package nsinit package main
import ( import (
"log" "log"
"os" "os"
"runtime"
"strconv" "strconv"
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
@ -23,7 +24,9 @@ var (
) )
func initAction(context *cli.Context) { func initAction(context *cli.Context) {
container, err := loadContainer() runtime.LockOSThread()
container, err := loadConfig()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -0,0 +1,67 @@
package main
import (
"log"
"os"
"strings"
"github.com/codegangsta/cli"
)
var (
logPath = os.Getenv("log")
argvs = make(map[string]*rFunc)
)
func init() {
argvs["exec"] = &rFunc{
Usage: "execute a process inside an existing container",
Action: nsenterExec,
}
argvs["mknod"] = &rFunc{
Usage: "mknod a device inside an existing container",
Action: nsenterMknod,
}
argvs["ip"] = &rFunc{
Usage: "display the container's network interfaces",
Action: nsenterIp,
}
}
func main() {
// we need to check our argv 0 for any registred functions to run instead of the
// normal cli code path
f, exists := argvs[strings.TrimPrefix(os.Args[0], "nsenter-")]
if exists {
runFunc(f)
return
}
app := cli.NewApp()
app.Name = "nsinit"
app.Version = "0.1"
app.Author = "libcontainer maintainers"
app.Flags = []cli.Flag{
cli.StringFlag{Name: "nspid"},
cli.StringFlag{Name: "console"},
}
app.Before = preload
app.Commands = []cli.Command{
execCommand,
initCommand,
statsCommand,
configCommand,
pauseCommand,
unpauseCommand,
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}

Binary file not shown.

Binary file not shown.

View File

@ -1,41 +1,84 @@
package nsinit package main
import ( import (
"fmt"
"log" "log"
"net"
"os"
"strconv"
"strings"
"text/tabwriter"
"github.com/codegangsta/cli" "github.com/docker/libcontainer"
"github.com/docker/libcontainer/devices"
"github.com/docker/libcontainer/mount/nodes"
"github.com/docker/libcontainer/namespaces" "github.com/docker/libcontainer/namespaces"
_ "github.com/docker/libcontainer/namespaces/nsenter"
) )
var nsenterCommand = cli.Command{ // nsenterExec exec's a process inside an existing container
Name: "nsenter", func nsenterExec(config *libcontainer.Config, args []string) {
Usage: "init process for entering an existing namespace", if err := namespaces.FinalizeSetns(config, args); err != nil {
Action: nsenterAction,
Flags: []cli.Flag{
cli.IntFlag{Name: "nspid"},
cli.StringFlag{Name: "containerjson"},
cli.StringFlag{Name: "console"},
},
}
func nsenterAction(context *cli.Context) {
args := context.Args()
if len(args) == 0 {
args = []string{"/bin/bash"}
}
container, err := loadContainerFromJson(context.String("containerjson"))
if err != nil {
log.Fatalf("unable to load container: %s", err)
}
nspid := context.Int("nspid")
if nspid <= 0 {
log.Fatalf("cannot enter into namespaces without valid pid: %q", nspid)
}
if err := namespaces.NsEnter(container, args); err != nil {
log.Fatalf("failed to nsenter: %s", err) log.Fatalf("failed to nsenter: %s", err)
} }
} }
// nsenterMknod runs mknod inside an existing container
//
// mknod <path> <type> <major> <minor>
func nsenterMknod(config *libcontainer.Config, args []string) {
if len(args) != 4 {
log.Fatalf("expected mknod to have 4 arguments not %d", len(args))
}
t := rune(args[1][0])
major, err := strconv.Atoi(args[2])
if err != nil {
log.Fatal(err)
}
minor, err := strconv.Atoi(args[3])
if err != nil {
log.Fatal(err)
}
n := &devices.Device{
Path: args[0],
Type: t,
MajorNumber: int64(major),
MinorNumber: int64(minor),
}
if err := nodes.CreateDeviceNode("/", n); err != nil {
log.Fatal(err)
}
}
// nsenterIp displays the network interfaces inside a container's net namespace
func nsenterIp(config *libcontainer.Config, args []string) {
interfaces, err := net.Interfaces()
if err != nil {
log.Fatal(err)
}
w := tabwriter.NewWriter(os.Stdout, 10, 1, 3, ' ', 0)
fmt.Fprint(w, "NAME\tMTU\tMAC\tFLAG\tADDRS\n")
for _, iface := range interfaces {
addrs, err := iface.Addrs()
if err != nil {
log.Fatal(err)
}
o := []string{}
for _, a := range addrs {
o = append(o, a.String())
}
fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\n", iface.Name, iface.MTU, iface.HardwareAddr, iface.Flags, strings.Join(o, ","))
}
w.Flush()
}

Binary file not shown.

View File

@ -1,7 +0,0 @@
package main
import "github.com/docker/libcontainer/nsinit"
func main() {
nsinit.NsInit()
}

View File

@ -1,4 +1,4 @@
package nsinit package main
import ( import (
"log" "log"
@ -34,7 +34,7 @@ func unpauseAction(context *cli.Context) {
} }
func toggle(state cgroups.FreezerState) error { func toggle(state cgroups.FreezerState) error {
container, err := loadContainer() container, err := loadConfig()
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,4 +1,4 @@
package nsinit package main
import ( import (
"encoding/json" "encoding/json"
@ -16,7 +16,7 @@ var statsCommand = cli.Command{
} }
func statsAction(context *cli.Context) { func statsAction(context *cli.Context) {
container, err := loadContainer() container, err := loadConfig()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -1,4 +1,4 @@
package nsinit package main
import ( import (
"encoding/json" "encoding/json"
@ -6,10 +6,18 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/codegangsta/cli"
"github.com/docker/libcontainer" "github.com/docker/libcontainer"
"github.com/docker/libcontainer/syncpipe"
) )
func loadContainer() (*libcontainer.Config, error) { // rFunc is a function registration for calling after an execin
type rFunc struct {
Usage string
Action func(*libcontainer.Config, []string)
}
func loadConfig() (*libcontainer.Config, error) {
f, err := os.Open(filepath.Join(dataPath, "container.json")) f, err := os.Open(filepath.Join(dataPath, "container.json"))
if err != nil { if err != nil {
return nil, err return nil, err
@ -35,12 +43,52 @@ func openLog(name string) error {
return nil return nil
} }
func loadContainerFromJson(rawData string) (*libcontainer.Config, error) { func findUserArgs() []string {
var container *libcontainer.Config i := 0
for _, a := range os.Args {
i++
if err := json.Unmarshal([]byte(rawData), &container); err != nil { if a == "--" {
break
}
}
return os.Args[i:]
}
// loadConfigFromFd loads a container's config from the sync pipe that is provided by
// fd 3 when running a process
func loadConfigFromFd() (*libcontainer.Config, error) {
syncPipe, err := syncpipe.NewSyncPipeFromFd(0, 3)
if err != nil {
return nil, err return nil, err
} }
return container, nil var config *libcontainer.Config
if err := syncPipe.ReadFromParent(&config); err != nil {
return nil, err
}
return config, nil
}
func preload(context *cli.Context) error {
if logPath != "" {
if err := openLog(logPath); err != nil {
return err
}
}
return nil
}
func runFunc(f *rFunc) {
userArgs := findUserArgs()
config, err := loadConfigFromFd()
if err != nil {
log.Fatalf("unable to receive config from sync pipe: %s", err)
}
f.Action(config, userArgs)
} }

Binary file not shown.

View File

@ -18,6 +18,9 @@ type State struct {
// Network runtime state. // Network runtime state.
NetworkState network.NetworkState `json:"network_state,omitempty"` NetworkState network.NetworkState `json:"network_state,omitempty"`
// Path to all the cgroups setup for a container. Key is cgroup subsystem name.
CgroupPaths map[string]string `json:"cgroup_paths,omitempty"`
} }
// The running state of the container. // The running state of the container.

View File

@ -6,8 +6,6 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"syscall" "syscall"
"github.com/docker/libcontainer/network"
) )
// SyncPipe allows communication to and from the child processes // SyncPipe allows communication to and from the child processes
@ -39,8 +37,8 @@ func (s *SyncPipe) Parent() *os.File {
return s.parent return s.parent
} }
func (s *SyncPipe) SendToChild(networkState *network.NetworkState) error { func (s *SyncPipe) SendToChild(v interface{}) error {
data, err := json.Marshal(networkState) data, err := json.Marshal(v)
if err != nil { if err != nil {
return err return err
} }
@ -63,18 +61,19 @@ func (s *SyncPipe) ReadFromChild() error {
return nil return nil
} }
func (s *SyncPipe) ReadFromParent() (*network.NetworkState, error) { func (s *SyncPipe) ReadFromParent(v interface{}) error {
data, err := ioutil.ReadAll(s.child) data, err := ioutil.ReadAll(s.child)
if err != nil { if err != nil {
return nil, fmt.Errorf("error reading from sync pipe %s", err) return fmt.Errorf("error reading from sync pipe %s", err)
} }
var networkState *network.NetworkState
if len(data) > 0 { if len(data) > 0 {
if err := json.Unmarshal(data, &networkState); err != nil { if err := json.Unmarshal(data, v); err != nil {
return nil, err return err
} }
} }
return networkState, nil
return nil
} }
func (s *SyncPipe) ReportChildError(err error) { func (s *SyncPipe) ReportChildError(err error) {

View File

@ -3,10 +3,12 @@ package syncpipe
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/docker/libcontainer/network"
) )
type testStruct struct {
Name string
}
func TestSendErrorFromChild(t *testing.T) { func TestSendErrorFromChild(t *testing.T) {
pipe, err := NewSyncPipe() pipe, err := NewSyncPipe()
if err != nil { if err != nil {
@ -46,16 +48,16 @@ func TestSendPayloadToChild(t *testing.T) {
expected := "libcontainer" expected := "libcontainer"
if err := pipe.SendToChild(&network.NetworkState{VethHost: expected}); err != nil { if err := pipe.SendToChild(testStruct{Name: expected}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
payload, err := pipe.ReadFromParent() var s *testStruct
if err != nil { if err := pipe.ReadFromParent(&s); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if payload.VethHost != expected { if s.Name != expected {
t.Fatalf("expected veth host %q but received %q", expected, payload.VethHost) t.Fatalf("expected name %q but received %q", expected, s.Name)
} }
} }

View File

@ -1,4 +1,4 @@
// +build linux,cgo // +build cgo
package system package system

View File

@ -1,4 +1,4 @@
// +build linux,!cgo // +build !cgo
package system package system

View File

@ -0,0 +1,18 @@
package libcontainer
import (
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/security/capabilities"
)
func GetAllCapabilities() []string {
return capabilities.GetAllCapabilities()
}
func DropBoundingSet(container *libcontainer.Config) error {
return capabilities.DropBoundingSet(container.Capabilities)
}
func DropCapabilities(container *libcontainer.Config) error {
return capabilities.DropCapabilities(container.Capabilities)
}

View File

@ -0,0 +1,18 @@
package libcontainer
import (
"github.com/docker/libcontainer"
"github.com/docker/libcontainer/security/capabilities"
)
func GetAllCapabilities() []string {
return capabilities.GetAllCapabilities()
}
func DropBoundingSet(container *libcontainer.Container) error {
return capabilities.DropBoundingSet(container.Capabilities)
}
func DropCapabilities(container *libcontainer.Container) error {
return capabilities.DropCapabilities(container.Capabilities)
}

View File

@ -0,0 +1 @@
Tianon Gravi <admwiggin@gmail.com> (@tianon)

View File

@ -0,0 +1,258 @@
package user
import (
"bufio"
"fmt"
"io"
"os"
"strconv"
"strings"
)
const (
minId = 0
maxId = 1<<31 - 1 //for 32-bit systems compatibility
)
var (
ErrRange = fmt.Errorf("Uids and gids must be in range %d-%d", minId, maxId)
)
type User struct {
Name string
Pass string
Uid int
Gid int
Gecos string
Home string
Shell string
}
type Group struct {
Name string
Pass string
Gid int
List []string
}
func parseLine(line string, v ...interface{}) {
if line == "" {
return
}
parts := strings.Split(line, ":")
for i, p := range parts {
if len(v) <= i {
// if we have more "parts" than we have places to put them, bail for great "tolerance" of naughty configuration files
break
}
switch e := v[i].(type) {
case *string:
// "root", "adm", "/bin/bash"
*e = p
case *int:
// "0", "4", "1000"
// ignore string to int conversion errors, for great "tolerance" of naughty configuration files
*e, _ = strconv.Atoi(p)
case *[]string:
// "", "root", "root,adm,daemon"
if p != "" {
*e = strings.Split(p, ",")
} else {
*e = []string{}
}
default:
// panic, because this is a programming/logic error, not a runtime one
panic("parseLine expects only pointers! argument " + strconv.Itoa(i) + " is not a pointer!")
}
}
}
func ParsePasswd() ([]*User, error) {
return ParsePasswdFilter(nil)
}
func ParsePasswdFilter(filter func(*User) bool) ([]*User, error) {
f, err := os.Open("/etc/passwd")
if err != nil {
return nil, err
}
defer f.Close()
return parsePasswdFile(f, filter)
}
func parsePasswdFile(r io.Reader, filter func(*User) bool) ([]*User, error) {
var (
s = bufio.NewScanner(r)
out = []*User{}
)
for s.Scan() {
if err := s.Err(); err != nil {
return nil, err
}
text := strings.TrimSpace(s.Text())
if text == "" {
continue
}
// see: man 5 passwd
// name:password:UID:GID:GECOS:directory:shell
// Name:Pass:Uid:Gid:Gecos:Home:Shell
// root:x:0:0:root:/root:/bin/bash
// adm:x:3:4:adm:/var/adm:/bin/false
p := &User{}
parseLine(
text,
&p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell,
)
if filter == nil || filter(p) {
out = append(out, p)
}
}
return out, nil
}
func ParseGroup() ([]*Group, error) {
return ParseGroupFilter(nil)
}
func ParseGroupFilter(filter func(*Group) bool) ([]*Group, error) {
f, err := os.Open("/etc/group")
if err != nil {
return nil, err
}
defer f.Close()
return parseGroupFile(f, filter)
}
func parseGroupFile(r io.Reader, filter func(*Group) bool) ([]*Group, error) {
var (
s = bufio.NewScanner(r)
out = []*Group{}
)
for s.Scan() {
if err := s.Err(); err != nil {
return nil, err
}
text := s.Text()
if text == "" {
continue
}
// see: man 5 group
// group_name:password:GID:user_list
// Name:Pass:Gid:List
// root:x:0:root
// adm:x:4:root,adm,daemon
p := &Group{}
parseLine(
text,
&p.Name, &p.Pass, &p.Gid, &p.List,
)
if filter == nil || filter(p) {
out = append(out, p)
}
}
return out, nil
}
// Given a string like "user", "1000", "user:group", "1000:1000", returns the uid, gid, list of supplementary group IDs, and home directory, if available and/or applicable.
func GetUserGroupSupplementaryHome(userSpec string, defaultUid, defaultGid int, defaultHome string) (int, int, []int, string, error) {
var (
uid = defaultUid
gid = defaultGid
suppGids = []int{}
home = defaultHome
userArg, groupArg string
)
// allow for userArg to have either "user" syntax, or optionally "user:group" syntax
parseLine(userSpec, &userArg, &groupArg)
users, err := ParsePasswdFilter(func(u *User) bool {
if userArg == "" {
return u.Uid == uid
}
return u.Name == userArg || strconv.Itoa(u.Uid) == userArg
})
if err != nil && !os.IsNotExist(err) {
if userArg == "" {
userArg = strconv.Itoa(uid)
}
return 0, 0, nil, "", fmt.Errorf("Unable to find user %v: %v", userArg, err)
}
haveUser := users != nil && len(users) > 0
if haveUser {
// if we found any user entries that matched our filter, let's take the first one as "correct"
uid = users[0].Uid
gid = users[0].Gid
home = users[0].Home
} else if userArg != "" {
// we asked for a user but didn't find them... let's check to see if we wanted a numeric user
uid, err = strconv.Atoi(userArg)
if err != nil {
// not numeric - we have to bail
return 0, 0, nil, "", fmt.Errorf("Unable to find user %v", userArg)
}
if uid < minId || uid > maxId {
return 0, 0, nil, "", ErrRange
}
// if userArg couldn't be found in /etc/passwd but is numeric, just roll with it - this is legit
}
if groupArg != "" || (haveUser && users[0].Name != "") {
groups, err := ParseGroupFilter(func(g *Group) bool {
if groupArg != "" {
return g.Name == groupArg || strconv.Itoa(g.Gid) == groupArg
}
for _, u := range g.List {
if u == users[0].Name {
return true
}
}
return false
})
if err != nil && !os.IsNotExist(err) {
return 0, 0, nil, "", fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err)
}
haveGroup := groups != nil && len(groups) > 0
if groupArg != "" {
if haveGroup {
// if we found any group entries that matched our filter, let's take the first one as "correct"
gid = groups[0].Gid
} else {
// we asked for a group but didn't find id... let's check to see if we wanted a numeric group
gid, err = strconv.Atoi(groupArg)
if err != nil {
// not numeric - we have to bail
return 0, 0, nil, "", fmt.Errorf("Unable to find group %v", groupArg)
}
if gid < minId || gid > maxId {
return 0, 0, nil, "", ErrRange
}
// if groupArg couldn't be found in /etc/group but is numeric, just roll with it - this is legit
}
} else if haveGroup {
suppGids = make([]int, len(groups))
for i, group := range groups {
suppGids[i] = group.Gid
}
}
}
return uid, gid, suppGids, home, nil
}

View File

@ -0,0 +1,94 @@
package user
import (
"strings"
"testing"
)
func TestUserParseLine(t *testing.T) {
var (
a, b string
c []string
d int
)
parseLine("", &a, &b)
if a != "" || b != "" {
t.Fatalf("a and b should be empty ('%v', '%v')", a, b)
}
parseLine("a", &a, &b)
if a != "a" || b != "" {
t.Fatalf("a should be 'a' and b should be empty ('%v', '%v')", a, b)
}
parseLine("bad boys:corny cows", &a, &b)
if a != "bad boys" || b != "corny cows" {
t.Fatalf("a should be 'bad boys' and b should be 'corny cows' ('%v', '%v')", a, b)
}
parseLine("", &c)
if len(c) != 0 {
t.Fatalf("c should be empty (%#v)", c)
}
parseLine("d,e,f:g:h:i,j,k", &c, &a, &b, &c)
if a != "g" || b != "h" || len(c) != 3 || c[0] != "i" || c[1] != "j" || c[2] != "k" {
t.Fatalf("a should be 'g', b should be 'h', and c should be ['i','j','k'] ('%v', '%v', '%#v')", a, b, c)
}
parseLine("::::::::::", &a, &b, &c)
if a != "" || b != "" || len(c) != 0 {
t.Fatalf("a, b, and c should all be empty ('%v', '%v', '%#v')", a, b, c)
}
parseLine("not a number", &d)
if d != 0 {
t.Fatalf("d should be 0 (%v)", d)
}
parseLine("b:12:c", &a, &d, &b)
if a != "b" || b != "c" || d != 12 {
t.Fatalf("a should be 'b' and b should be 'c', and d should be 12 ('%v', '%v', %v)", a, b, d)
}
}
func TestUserParsePasswd(t *testing.T) {
users, err := parsePasswdFile(strings.NewReader(`
root:x:0:0:root:/root:/bin/bash
adm:x:3:4:adm:/var/adm:/bin/false
this is just some garbage data
`), nil)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(users) != 3 {
t.Fatalf("Expected 3 users, got %v", len(users))
}
if users[0].Uid != 0 || users[0].Name != "root" {
t.Fatalf("Expected users[0] to be 0 - root, got %v - %v", users[0].Uid, users[0].Name)
}
if users[1].Uid != 3 || users[1].Name != "adm" {
t.Fatalf("Expected users[1] to be 3 - adm, got %v - %v", users[1].Uid, users[1].Name)
}
}
func TestUserParseGroup(t *testing.T) {
groups, err := parseGroupFile(strings.NewReader(`
root:x:0:root
adm:x:4:root,adm,daemon
this is just some garbage data
`), nil)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(groups) != 3 {
t.Fatalf("Expected 3 groups, got %v", len(groups))
}
if groups[0].Gid != 0 || groups[0].Name != "root" || len(groups[0].List) != 1 {
t.Fatalf("Expected groups[0] to be 0 - root - 1 member, got %v - %v - %v", groups[0].Gid, groups[0].Name, len(groups[0].List))
}
if groups[1].Gid != 4 || groups[1].Name != "adm" || len(groups[1].List) != 3 {
t.Fatalf("Expected groups[1] to be 4 - adm - 3 members, got %v - %v - %v", groups[1].Gid, groups[1].Name, len(groups[1].List))
}
}

View File

@ -4,4 +4,4 @@ set -e
set -x set -x
# Statically build cAdvisor from source and stage it. # Statically build cAdvisor from source and stage it.
godep go build --ldflags '-extldflags "-static"' github.com/google/cadvisor godep go build -a --ldflags '-extldflags "-static"' github.com/google/cadvisor