diff --git a/client/driver/exec.go b/client/driver/exec.go index e1fdb1646c3a..efa846619a8d 100644 --- a/client/driver/exec.go +++ b/client/driver/exec.go @@ -5,7 +5,6 @@ import ( "fmt" "log" "path/filepath" - "runtime" "syscall" "time" @@ -13,7 +12,6 @@ import ( "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/driver/executor" cstructs "github.com/hashicorp/nomad/client/driver/structs" - "github.com/hashicorp/nomad/client/fingerprint" "github.com/hashicorp/nomad/client/getter" "github.com/hashicorp/nomad/nomad/structs" "github.com/mitchellh/mapstructure" @@ -23,8 +21,8 @@ import ( // features. type ExecDriver struct { DriverContext - fingerprint.StaticFingerprinter } + type ExecDriverConfig struct { ArtifactSource string `mapstructure:"artifact_source"` Checksum string `mapstructure:"checksum"` @@ -47,9 +45,9 @@ func NewExecDriver(ctx *DriverContext) Driver { } func (d *ExecDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { - // Only enable if we are root on linux. - if runtime.GOOS != "linux" { - d.logger.Printf("[DEBUG] driver.exec: only available on linux, disabling") + // Only enable if cgroups are available and we are root + if _, ok := node.Attributes["unique.cgroup.mountpoint"]; !ok { + d.logger.Printf("[DEBUG] driver.exec: cgroups unavailable, disabling") return false, nil } else if syscall.Geteuid() != 0 { d.logger.Printf("[DEBUG] driver.exec: must run as root user, disabling") @@ -60,6 +58,10 @@ func (d *ExecDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, return true, nil } +func (d *ExecDriver) Periodic() (bool, time.Duration) { + return true, 15 * time.Second +} + func (d *ExecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) { var driverConfig ExecDriverConfig if err := mapstructure.WeakDecode(task.Config, &driverConfig); err != nil { diff --git a/client/driver/exec_test.go b/client/driver/exec_test.go index 9c6cf8649927..5b077e9d699f 100644 --- a/client/driver/exec_test.go +++ b/client/driver/exec_test.go @@ -22,7 +22,9 @@ func TestExecDriver_Fingerprint(t *testing.T) { driverCtx, _ := testDriverContexts(&structs.Task{Name: "foo"}) d := NewExecDriver(driverCtx) node := &structs.Node{ - Attributes: make(map[string]string), + Attributes: map[string]string{ + "unique.cgroup.mountpoint": "/sys/fs/cgroup", + }, } apply, err := d.Fingerprint(&config.Config{}, node) if err != nil { diff --git a/client/fingerprint/cgroup.go b/client/fingerprint/cgroup.go new file mode 100644 index 000000000000..3fb14a2f3b67 --- /dev/null +++ b/client/fingerprint/cgroup.go @@ -0,0 +1,88 @@ +package fingerprint + +import ( + "fmt" + "log" + "time" + + client "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/nomad/structs" +) + +const ( + cgroupAvailable = "available" + cgroupUnavailable = "unavailable" + interval = 15 +) + +type CGroupFingerprint struct { + logger *log.Logger + lastState string + mountPointDetector MountPointDetector +} + +// An interface to isolate calls to the cgroup library +// This facilitates testing where we can implement +// fake mount points to test various code paths +type MountPointDetector interface { + MountPoint() (string, error) +} + +// Implements the interface detector which calls the cgroups library directly +type DefaultMountPointDetector struct { +} + +// Call out to the default cgroup library +func (b *DefaultMountPointDetector) MountPoint() (string, error) { + return FindCgroupMountpointDir() +} + +// NewCGroupFingerprint returns a new cgroup fingerprinter +func NewCGroupFingerprint(logger *log.Logger) Fingerprint { + f := &CGroupFingerprint{ + logger: logger, + lastState: cgroupUnavailable, + mountPointDetector: &DefaultMountPointDetector{}, + } + return f +} + +// Fingerprint tries to find a valid cgroup moint point +func (f *CGroupFingerprint) Fingerprint(cfg *client.Config, node *structs.Node) (bool, error) { + mount, err := f.mountPointDetector.MountPoint() + if err != nil { + f.clearCGroupAttributes(node) + return false, fmt.Errorf("Failed to discover cgroup mount point: %s", err) + } + + // Check if a cgroup mount point was found + if mount == "" { + // Clear any attributes from the previous fingerprint. + f.clearCGroupAttributes(node) + + if f.lastState == cgroupAvailable { + f.logger.Printf("[INFO] fingerprint.cgroups: cgroups are unavailable") + } + f.lastState = cgroupUnavailable + return true, nil + } + + node.Attributes["unique.cgroup.mountpoint"] = mount + + if f.lastState == cgroupUnavailable { + f.logger.Printf("[INFO] fingerprint.cgroups: cgroups are available") + } + f.lastState = cgroupAvailable + return true, nil +} + +// clearCGroupAttributes clears any node attributes related to cgroups that might +// have been set in a previous fingerprint run. +func (f *CGroupFingerprint) clearCGroupAttributes(n *structs.Node) { + delete(n.Attributes, "unique.cgroup.mountpoint") +} + +// Periodic determines the interval at which the periodic fingerprinter will run. +func (f *CGroupFingerprint) Periodic() (bool, time.Duration) { + return true, interval * time.Second +} diff --git a/client/fingerprint/cgroup_linux.go b/client/fingerprint/cgroup_linux.go new file mode 100644 index 000000000000..25171c1f4c36 --- /dev/null +++ b/client/fingerprint/cgroup_linux.go @@ -0,0 +1,24 @@ +// +build linux + +package fingerprint + +import ( + "github.com/opencontainers/runc/libcontainer/cgroups" +) + +// FindCgroupMountpointDir is used to find the cgroup mount point on a Linux +// system. +func FindCgroupMountpointDir() (string, error) { + mount, err := cgroups.FindCgroupMountpointDir() + if err != nil { + switch e := err.(type) { + case *cgroups.NotFoundError: + // It's okay if the mount point is not discovered + return "", nil + default: + // All other errors are passed back as is + return "", e + } + } + return mount, nil +} diff --git a/client/fingerprint/cgroup_test.go b/client/fingerprint/cgroup_test.go new file mode 100644 index 000000000000..be5d251e0809 --- /dev/null +++ b/client/fingerprint/cgroup_test.go @@ -0,0 +1,100 @@ +package fingerprint + +import ( + "fmt" + "testing" + + "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/nomad/structs" +) + +// A fake mount point detector that returns an empty path +type MountPointDetectorNoMountPoint struct{} + +func (m *MountPointDetectorNoMountPoint) MountPoint() (string, error) { + return "", nil +} + +// A fake mount point detector that returns an error +type MountPointDetectorMountPointFail struct{} + +func (m *MountPointDetectorMountPointFail) MountPoint() (string, error) { + return "", fmt.Errorf("cgroup mountpoint discovery failed") +} + +// A fake mount point detector that returns a valid path +type MountPointDetectorValidMountPoint struct{} + +func (m *MountPointDetectorValidMountPoint) MountPoint() (string, error) { + return "/sys/fs/cgroup", nil +} + +// A fake mount point detector that returns an empty path +type MountPointDetectorEmptyMountPoint struct{} + +func (m *MountPointDetectorEmptyMountPoint) MountPoint() (string, error) { + return "", nil +} + +func TestCGroupFingerprint(t *testing.T) { + f := &CGroupFingerprint{ + logger: testLogger(), + lastState: cgroupUnavailable, + mountPointDetector: &MountPointDetectorMountPointFail{}, + } + + node := &structs.Node{ + Attributes: make(map[string]string), + } + + ok, err := f.Fingerprint(&config.Config{}, node) + if err == nil { + t.Fatalf("expected an error") + } + if ok { + t.Fatalf("should not apply") + } + if a, ok := node.Attributes["unique.cgroup.mountpoint"]; ok { + t.Fatalf("unexpected attribute found, %s", a) + } + + f = &CGroupFingerprint{ + logger: testLogger(), + lastState: cgroupUnavailable, + mountPointDetector: &MountPointDetectorValidMountPoint{}, + } + + node = &structs.Node{ + Attributes: make(map[string]string), + } + + ok, err = f.Fingerprint(&config.Config{}, node) + if err != nil { + t.Fatalf("unexpected error, %s", err) + } + if !ok { + t.Fatalf("should apply") + } + assertNodeAttributeContains(t, node, "unique.cgroup.mountpoint") + + f = &CGroupFingerprint{ + logger: testLogger(), + lastState: cgroupUnavailable, + mountPointDetector: &MountPointDetectorEmptyMountPoint{}, + } + + node = &structs.Node{ + Attributes: make(map[string]string), + } + + ok, err = f.Fingerprint(&config.Config{}, node) + if err != nil { + t.Fatalf("unexpected error, %s", err) + } + if !ok { + t.Fatalf("should apply") + } + if a, ok := node.Attributes["unique.cgroup.mountpoint"]; ok { + t.Fatalf("unexpected attribute found, %s", a) + } +} diff --git a/client/fingerprint/cgroup_universal.go b/client/fingerprint/cgroup_universal.go new file mode 100644 index 000000000000..eeeade435d24 --- /dev/null +++ b/client/fingerprint/cgroup_universal.go @@ -0,0 +1,8 @@ +// +build !linux + +package fingerprint + +// FindCgroupMountpointDir returns an empty path on non-Linux systems +func FindCgroupMountpointDir() (string, error) { + return "", nil +} diff --git a/client/fingerprint/fingerprint.go b/client/fingerprint/fingerprint.go index a0139d485cb4..c9195253ba3d 100644 --- a/client/fingerprint/fingerprint.go +++ b/client/fingerprint/fingerprint.go @@ -12,10 +12,11 @@ import ( // EmptyDuration is to be used by fingerprinters that are not periodic. const EmptyDuration = time.Duration(0) -// BuiltinFingerprints is a slice containing the key names of all regestered +// BuiltinFingerprints is a slice containing the key names of all registered // fingerprints available, to provided an ordered iteration var BuiltinFingerprints = []string{ "arch", + "cgroup", "consul", "cpu", "env_aws", @@ -30,6 +31,7 @@ var BuiltinFingerprints = []string{ // which are available, corresponding to a key found in BuiltinFingerprints var builtinFingerprintMap = map[string]Factory{ "arch": NewArchFingerprint, + "cgroup": NewCGroupFingerprint, "consul": NewConsulFingerprint, "cpu": NewCPUFingerprint, "env_aws": NewEnvAWSFingerprint,