Skip to content

Commit

Permalink
remove attributes from periodic fingerprints when state changes
Browse files Browse the repository at this point in the history
write test for client periodic fingerprinters
  • Loading branch information
chelseakomlo committed Jan 29, 2018
1 parent f5fc20a commit ae889b4
Show file tree
Hide file tree
Showing 14 changed files with 162 additions and 4 deletions.
41 changes: 41 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/hashicorp/consul/lib/freeport"
memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/driver"
"github.com/hashicorp/nomad/client/fingerprint"
"github.com/hashicorp/nomad/command/agent/consul"
"github.com/hashicorp/nomad/helper"
Expand Down Expand Up @@ -252,6 +253,46 @@ func TestClient_HasNodeChanged(t *testing.T) {
}
}

func TestClient_Fingerprint_Periodic(t *testing.T) {
if _, ok := driver.BuiltinDrivers["mock_driver"]; !ok {
t.Skip(`test requires mock_driver; run with "-tags nomad_test"`)
}
t.Parallel()

c1 := testClient(t, func(c *config.Config) {
c.Options = map[string]string{
"test.shutdown_periodic_after": "true",
"test.shutdown_periodic_duration": "3",
}
})
defer c1.Shutdown()

node := c1.config.Node
mockDriverName := "driver.mock_driver"

// Ensure the mock driver is registered on the client
testutil.WaitForResult(func() (bool, error) {
mockDriverStatus := node.Attributes[mockDriverName]
if mockDriverStatus == "" {
return false, fmt.Errorf("mock driver attribute should be set on the client")
}
return true, nil
}, func(err error) {
t.Fatalf("err: %v", err)
})

// Ensure that the client fingerprinter eventually removes this attribute
testutil.WaitForResult(func() (bool, error) {
mockDriverStatus := node.Attributes[mockDriverName]
if mockDriverStatus != "" {
return false, fmt.Errorf("mock driver attribute should not be set on the client")
}
return true, nil
}, func(err error) {
t.Fatalf("err: %v", err)
})
}

func TestClient_Fingerprint_InWhitelist(t *testing.T) {
t.Parallel()
c := testClient(t, func(c *config.Config) {
Expand Down
2 changes: 2 additions & 0 deletions client/driver/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ func (d *DockerDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstru
d.logger.Printf("[INFO] driver.docker: failed to initialize client: %s", err)
}
d.fingerprintSuccess = helper.BoolToPtr(false)
resp.RemoveAttribute(dockerDriverAttr)
return nil
}

Expand All @@ -494,6 +495,7 @@ func (d *DockerDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstru
d.logger.Printf("[DEBUG] driver.docker: could not connect to docker daemon at %s: %s", client.Endpoint(), err)
}
d.fingerprintSuccess = helper.BoolToPtr(false)
resp.RemoveAttribute(dockerDriverAttr)
return nil
}

Expand Down
2 changes: 2 additions & 0 deletions client/driver/exec_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ func (d *ExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct
d.logger.Printf("[DEBUG] driver.exec: cgroups unavailable, disabling")
}
d.fingerprintSuccess = helper.BoolToPtr(false)
resp.RemoveAttribute(execDriverAttr)
return nil
} else if unix.Geteuid() != 0 {
if d.fingerprintSuccess == nil || *d.fingerprintSuccess {
d.logger.Printf("[DEBUG] driver.exec: must run as root user, disabling")
}
d.fingerprintSuccess = helper.BoolToPtr(false)
resp.RemoveAttribute(execDriverAttr)
return nil
}

Expand Down
3 changes: 3 additions & 0 deletions client/driver/java.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct
d.logger.Printf("[DEBUG] driver.java: root privileges and mounted cgroups required on linux, disabling")
}
d.fingerprintSuccess = helper.BoolToPtr(false)
resp.RemoveAttribute(javaDriverAttr)
return nil
}

Expand All @@ -131,6 +132,7 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct
if err != nil {
// assume Java wasn't found
d.fingerprintSuccess = helper.BoolToPtr(false)
resp.RemoveAttribute(javaDriverAttr)
return nil
}

Expand All @@ -150,6 +152,7 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct
d.logger.Println("[WARN] driver.java: error parsing Java version information, aborting")
}
d.fingerprintSuccess = helper.BoolToPtr(false)
resp.RemoveAttribute(javaDriverAttr)
return nil
}

Expand Down
46 changes: 43 additions & 3 deletions client/driver/mock_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,30 @@ type MockDriverConfig struct {
// MockDriver is a driver which is used for testing purposes
type MockDriver struct {
DriverContext
fingerprint.StaticFingerprinter

// isShutdown is an internal concept to use to track whether the driver
// should be shut down
isShutdown bool

cleanupFailNum int
}

// NewMockDriver is a factory method which returns a new Mock Driver
func NewMockDriver(ctx *DriverContext) Driver {
return &MockDriver{DriverContext: *ctx}
md := &MockDriver{DriverContext: *ctx}

// if the shutdown configuration options are set, start the timer here.
// This config option defaults to false
if ctx.config != nil && ctx.config.ReadBoolDefault(fingerprint.ShutdownPeriodicAfter, false) {
duration, err := ctx.config.ReadInt(fingerprint.ShutdownPeriodicDuration)
if err != nil {
errMsg := fmt.Sprintf("unable to read config option for shutdown_periodic_duration %s, got err %s", duration, err.Error())
panic(errMsg)
}
go md.startShutdownTimer(duration)
}

return md
}

func (d *MockDriver) Abilities() DriverAbilities {
Expand Down Expand Up @@ -165,6 +181,20 @@ func (m *MockDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse
return &StartResponse{Handle: &h, Network: net}, nil
}

// startShutdownTimer sets a timer, after which the mock driver will no loger be
// responsive. This is used for testing periodic fingerprinting functionality
func (m *MockDriver) startShutdownTimer(duration int) {
timer := time.NewTimer(time.Duration(duration) * time.Second)
for {
select {
case <-timer.C:
m.isShutdown = true
default:
time.Sleep(100 * time.Millisecond)
}
}
}

// Cleanup deletes all keys except for Config.Options["cleanup_fail_on"] for
// Config.Options["cleanup_fail_num"] times. For failures it will return a
// recoverable error.
Expand Down Expand Up @@ -194,7 +224,12 @@ func (m *MockDriver) Validate(map[string]interface{}) error {

// Fingerprint fingerprints a node and returns if MockDriver is enabled
func (m *MockDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error {
resp.AddAttribute("driver.mock_driver", "1")
switch {
case m.isShutdown:
resp.RemoveAttribute("driver.mock_driver")
default:
resp.AddAttribute("driver.mock_driver", "1")
}
return nil
}

Expand Down Expand Up @@ -338,3 +373,8 @@ func (h *mockDriverHandle) run() {
}
}
}

// When testing, poll for updates
func (m *MockDriver) Periodic() (bool, time.Duration) {
return true, 500 * time.Millisecond
}
3 changes: 3 additions & 0 deletions client/driver/qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,15 @@ func (d *QemuDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct
}
outBytes, err := exec.Command(bin, "--version").Output()
if err != nil {
// return no error, as it isn't an error to not find qemu, it just means we
// can't use it.
return nil
}
out := strings.TrimSpace(string(outBytes))

matches := reQemuVersion.FindStringSubmatch(out)
if len(matches) != 2 {
resp.RemoveAttribute(qemuDriverAttr)
return fmt.Errorf("Unable to parse Qemu version string: %#v", matches)
}
currentQemuVersion := matches[1]
Expand Down
1 change: 1 addition & 0 deletions client/driver/raw_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ func (d *RawExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstr
return nil
}

resp.RemoveAttribute(rawExecDriverAttr)
return nil
}

Expand Down
3 changes: 3 additions & 0 deletions client/driver/rkt.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ func (d *RktDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs
d.logger.Printf("[DEBUG] driver.rkt: must run as root user, disabling")
}
d.fingerprintSuccess = helper.BoolToPtr(false)
resp.RemoveAttribute(rktDriverAttr)
return nil
}

Expand All @@ -332,6 +333,7 @@ func (d *RktDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs
appcMatches := reAppcVersion.FindStringSubmatch(out)
if len(rktMatches) != 2 || len(appcMatches) != 2 {
d.fingerprintSuccess = helper.BoolToPtr(false)
resp.RemoveAttribute(rktDriverAttr)
return fmt.Errorf("Unable to parse Rkt version string: %#v", rktMatches)
}

Expand All @@ -345,6 +347,7 @@ func (d *RktDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs
currentVersion, minVersion)
}
d.fingerprintSuccess = helper.BoolToPtr(false)
resp.RemoveAttribute(rktDriverAttr)
return nil
}

Expand Down
8 changes: 8 additions & 0 deletions client/fingerprint/cgroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package fingerprint
import (
"log"
"time"

cstructs "github.com/hashicorp/nomad/client/structs"
)

const (
Expand Down Expand Up @@ -45,6 +47,12 @@ func NewCGroupFingerprint(logger *log.Logger) Fingerprint {
return f
}

// clearCGroupAttributes clears any node attributes related to cgroups that might
// have been set in a previous fingerprint run.
func (f *CGroupFingerprint) clearCGroupAttributes(r *cstructs.FingerprintResponse) {
r.RemoveAttribute("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
Expand Down
4 changes: 4 additions & 0 deletions client/fingerprint/cgroup_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ func FindCgroupMountpointDir() (string, error) {
func (f *CGroupFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error {
mount, err := f.mountPointDetector.MountPoint()
if err != nil {
f.clearCGroupAttributes(resp)
return fmt.Errorf("Failed to discover cgroup mount point: %s", err)
}

// Check if a cgroup mount point was found
if mount == "" {

f.clearCGroupAttributes(resp)

if f.lastState == cgroupAvailable {
f.logger.Printf("[INFO] fingerprint.cgroups: cgroups are unavailable")
}
Expand Down
13 changes: 12 additions & 1 deletion client/fingerprint/consul.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (f *ConsulFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *
// If we can't hit this URL consul is probably not running on this machine.
info, err := f.client.Agent().Self()
if err != nil {
// TODO this should set consul in the response if the error is not nil
f.clearConsulAttributes(resp)

// Print a message indicating that the Consul Agent is not available
// anymore
Expand Down Expand Up @@ -102,6 +102,17 @@ func (f *ConsulFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *
return nil
}

// clearConsulAttributes removes consul attributes and links from the passed
// Node.
func (f *ConsulFingerprint) clearConsulAttributes(r *cstructs.FingerprintResponse) {
r.RemoveAttribute("consul.server")
r.RemoveAttribute("consul.version")
r.RemoveAttribute("consul.revision")
r.RemoveAttribute("unique.consul.name")
r.RemoveAttribute("consul.datacenter")
r.RemoveLink("consul")
}

func (f *ConsulFingerprint) Periodic() (bool, time.Duration) {
return true, 15 * time.Second
}
10 changes: 10 additions & 0 deletions client/fingerprint/fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ const (
// TightenNetworkTimeoutsConfig is a config key that can be used during
// tests to tighten the timeouts for fingerprinters that make network calls.
TightenNetworkTimeoutsConfig = "test.tighten_network_timeouts"

// ShutdownPeriodicAfter is a config key that can be used during tests to
// "stop" a previously-functioning driver, allowing for testing of periodic
// drivers and fingerprinters
ShutdownPeriodicAfter = "test.shutdown_periodic_after"

// ShutdownPeriodicDuration is a config option that can be used during tests
// to "stop" a previously functioning driver after the specified duration
// (specified in seconds) for testing of periodic drivers and fingerprinters.
ShutdownPeriodicDuration = "test.shutdown_periodic_duration"
)

func init() {
Expand Down
8 changes: 8 additions & 0 deletions client/fingerprint/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func (f *VaultFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *c
// Connect to vault and parse its information
status, err := f.client.Sys().SealStatus()
if err != nil {
f.clearVaultAttributes(resp)
// Print a message indicating that Vault is not available anymore
if f.lastState == vaultAvailable {
f.logger.Printf("[INFO] fingerprint.vault: Vault is unavailable")
Expand Down Expand Up @@ -79,3 +80,10 @@ func (f *VaultFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *c
func (f *VaultFingerprint) Periodic() (bool, time.Duration) {
return true, 15 * time.Second
}

func (f *VaultFingerprint) clearVaultAttributes(r *cstructs.FingerprintResponse) {
r.RemoveAttribute("vault.accessible")
r.RemoveAttribute("vault.version")
r.RemoveAttribute("vault.cluster_id")
r.RemoveAttribute("vault.cluster_name")
}
22 changes: 22 additions & 0 deletions client/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@ func (f *FingerprintResponse) AddAttribute(name, value string) {
f.attributes[name] = value
}

// RemoveAttribute sets the given attribute to empty, which will later remove
// it entirely from the node
func (f *FingerprintResponse) RemoveAttribute(name string) {
// initialize attributes if it has not been already
if f.attributes == nil {
f.attributes = make(map[string]string, 0)
}

f.attributes[name] = ""
}

// GetAttributes fetches the attributes for the fingerprint response
func (f *FingerprintResponse) GetAttributes() map[string]string {
// initialize attributes if it has not been already
Expand All @@ -230,6 +241,17 @@ func (f *FingerprintResponse) AddLink(name, value string) {
f.links[name] = value
}

// RemoveLink removes a link entry from the fingerprint response. This will
// later remove it entirely from the node
func (f *FingerprintResponse) RemoveLink(name string) {
// initialize links if it has not been already
if f.links == nil {
f.links = make(map[string]string, 0)
}

f.links[name] = ""
}

// GetLinks returns the links for the fingerprint response
func (f *FingerprintResponse) GetLinks() map[string]string {
// initialize links if it has not been already
Expand Down

0 comments on commit ae889b4

Please sign in to comment.