From cc1213a8bbdf0e326f1adc22a0ca099470b5f848 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Wed, 24 Jan 2018 08:01:37 -0500 Subject: [PATCH] add fingerprint manager --- client/client.go | 116 ++++++------------------ client/fingerprint_manager.go | 122 +++++++++++++++++++++++++ client/fingerprint_manager_test.go | 139 +++++++++++++++++++++++++++++ 3 files changed, 289 insertions(+), 88 deletions(-) create mode 100644 client/fingerprint_manager.go create mode 100644 client/fingerprint_manager_test.go diff --git a/client/client.go b/client/client.go index 4054fca6b..9cc72a5af 100644 --- a/client/client.go +++ b/client/client.go @@ -230,13 +230,24 @@ func NewClient(cfg *config.Config, consulCatalog consul.CatalogAPI, consulServic return nil, fmt.Errorf("node setup failed: %v", err) } + fingerprintManager := &FingerprintManager{ + getConfig: func() *config.Config { + c.configLock.Lock() + defer c.configLock.Unlock() + return c.config + }, + node: c.config.Node, + client: c, + logger: c.logger, + } + // Fingerprint the node - if err := c.fingerprint(); err != nil { + if err := c.fingerprint(fingerprintManager); err != nil { return nil, fmt.Errorf("fingerprinting failed: %v", err) } // Scan for drivers - if err := c.setupDrivers(); err != nil { + if err := c.setupDrivers(fingerprintManager); err != nil { return nil, fmt.Errorf("driver setup failed: %v", err) } @@ -925,14 +936,14 @@ func (c *Client) reservePorts() { } // fingerprint is used to fingerprint the client and setup the node -func (c *Client) fingerprint() error { +func (c *Client) fingerprint(fingerprintManager *FingerprintManager) error { whitelist := c.config.ReadStringListToMap("fingerprint.whitelist") whitelistEnabled := len(whitelist) > 0 blacklist := c.config.ReadStringListToMap("fingerprint.blacklist") c.logger.Printf("[DEBUG] client: built-in fingerprints: %v", fingerprint.BuiltinFingerprints()) - var detectedFingerprints []string + var availableFingerprints []string var skippedFingerprints []string for _, name := range fingerprint.BuiltinFingerprints() { // Skip modules that are not in the whitelist if it is enabled. @@ -946,78 +957,29 @@ func (c *Client) fingerprint() error { continue } - f, err := fingerprint.NewFingerprint(name, c.logger) - if err != nil { - return err - } - - c.configLock.Lock() - request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} - var response cstructs.FingerprintResponse - err = f.Fingerprint(request, &response) - c.configLock.Unlock() - if err != nil { - return err - } - - // log the fingerprinters which have been applied - if response.Detected { - detectedFingerprints = append(detectedFingerprints, name) - } - - // add the diff found from each fingerprinter - c.updateNodeFromFingerprint(&response) - - p, period := f.Periodic() - if p { - // TODO: If more periodic fingerprinters are added, then - // fingerprintPeriodic should be used to handle all the periodic - // fingerprinters by using a priority queue. - go c.fingerprintPeriodic(name, f, period) - } + availableFingerprints = append(availableFingerprints, name) + } + + if err := fingerprintManager.SetupFingerprints(availableFingerprints); err != nil { + return err } - c.logger.Printf("[DEBUG] client: detected fingerprints %v", detectedFingerprints) if len(skippedFingerprints) != 0 { c.logger.Printf("[DEBUG] client: fingerprint modules skipped due to white/blacklist: %v", skippedFingerprints) } return nil } -// fingerprintPeriodic runs a fingerprinter at the specified duration. -func (c *Client) fingerprintPeriodic(name string, f fingerprint.Fingerprint, d time.Duration) { - c.logger.Printf("[DEBUG] client: fingerprinting %v every %v", name, d) - for { - select { - case <-time.After(d): - c.configLock.Lock() - request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} - var response cstructs.FingerprintResponse - err := f.Fingerprint(request, &response) - c.configLock.Unlock() - - if err != nil { - c.logger.Printf("[DEBUG] client: periodic fingerprinting for %v failed: %v", name, err) - } else { - c.updateNodeFromFingerprint(&response) - } - - case <-c.shutdownCh: - return - } - } -} - // setupDrivers is used to find the available drivers -func (c *Client) setupDrivers() error { +func (c *Client) setupDrivers(fingerprintManager *FingerprintManager) error { // Build the white/blacklists of drivers. whitelist := c.config.ReadStringListToMap("driver.whitelist") whitelistEnabled := len(whitelist) > 0 blacklist := c.config.ReadStringListToMap("driver.blacklist") - var detectedDrivers []string + var availDrivers []string var skippedDrivers []string - driverCtx := driver.NewDriverContext("", "", c.config, c.config.Node, c.logger, nil) + for name := range driver.BuiltinDrivers { // Skip fingerprinting drivers that are not in the whitelist if it is // enabled. @@ -1031,35 +993,13 @@ func (c *Client) setupDrivers() error { continue } - d, err := driver.NewDriver(name, driverCtx) - if err != nil { - return err - } - - c.configLock.Lock() - request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} - var response cstructs.FingerprintResponse - err = d.Fingerprint(request, &response) - c.configLock.Unlock() - if err != nil { - return err - } - - // log the fingerprinters which have been applied - if response.Detected { - detectedDrivers = append(detectedDrivers, name) - } - - c.updateNodeFromFingerprint(&response) - - p, period := d.Periodic() - if p { - go c.fingerprintPeriodic(name, d, period) - } - + availDrivers = append(availDrivers, name) + } + + if err := fingerprintManager.SetupDrivers(availDrivers); err != nil { + return err } - c.logger.Printf("[DEBUG] client: detected drivers %v", detectedDrivers) if len(skippedDrivers) > 0 { c.logger.Printf("[DEBUG] client: drivers skipped due to white/blacklist: %v", skippedDrivers) } diff --git a/client/fingerprint_manager.go b/client/fingerprint_manager.go new file mode 100644 index 000000000..c0c438827 --- /dev/null +++ b/client/fingerprint_manager.go @@ -0,0 +1,122 @@ +package client + +import ( + "log" + "time" + + "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/client/driver" + "github.com/hashicorp/nomad/client/fingerprint" + cstructs "github.com/hashicorp/nomad/client/structs" + "github.com/hashicorp/nomad/nomad/structs" +) + +// FingerprintManager runs a client fingerprinters on a continuous basis, and +// updates the client when the node has changed +type FingerprintManager struct { + getConfig func() *config.Config + node *structs.Node + client *Client + logger *log.Logger +} + +// run runs each fingerprinter individually on an ongoing basis +func (fm *FingerprintManager) run(f fingerprint.Fingerprint, period time.Duration, name string) { + fm.logger.Printf("[DEBUG] fingerprint_manager: fingerprinting %s every %v", name, period) + + for { + select { + case <-time.After(period): + _, err := fm.fingerprint(name, f) + if err != nil { + fm.logger.Printf("[DEBUG] fingerprint_manager: periodic fingerprinting for %v failed: %+v", name, err) + continue + } + + case <-fm.client.shutdownCh: + return + } + } +} + +// setupDrivers is used to fingerprint the node to see if these drivers are +// supported +func (fm *FingerprintManager) SetupDrivers(drivers []string) error { + var availDrivers []string + driverCtx := driver.NewDriverContext("", "", fm.getConfig(), fm.node, fm.logger, nil) + for _, name := range drivers { + + d, err := driver.NewDriver(name, driverCtx) + if err != nil { + return err + } + + detected, err := fm.fingerprint(name, d) + if err != nil { + fm.logger.Printf("[DEBUG] fingerprint_manager: fingerprinting for %v failed: %+v", name, err) + return err + } + + // log the fingerprinters which have been applied + if detected { + availDrivers = append(availDrivers, name) + } + + p, period := d.Periodic() + if p { + go fm.run(d, period, name) + } + } + + fm.logger.Printf("[DEBUG] fingerprint_manager: available drivers %v", availDrivers) + + return nil +} + +// fingerprint does an initial fingerprint of the client. If the fingerprinter +// is meant to be run continuously, a process is launched ro perform this +// fingerprint on an ongoing basis in the background. +func (fm *FingerprintManager) fingerprint(name string, f fingerprint.Fingerprint) (bool, error) { + request := &cstructs.FingerprintRequest{Config: fm.getConfig(), Node: fm.node} + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) + if err != nil { + return false, err + } + + fm.client.updateNodeFromFingerprint(&response) + return response.Detected, nil +} + +// setupDrivers is used to fingerprint the node to see if these attributes are +// supported +func (fm *FingerprintManager) SetupFingerprints(fingerprints []string) error { + var appliedFingerprints []string + + for _, name := range fingerprints { + f, err := fingerprint.NewFingerprint(name, fm.logger) + + if err != nil { + fm.logger.Printf("[DEBUG] fingerprint_manager: fingerprinting for %v failed: %+v", name, err) + return err + } + + detected, err := fm.fingerprint(name, f) + if err != nil { + return err + } + + // log the fingerprinters which have been applied + if detected { + appliedFingerprints = append(appliedFingerprints, name) + } + + p, period := f.Periodic() + if p { + go fm.run(f, period, name) + } + } + + fm.logger.Printf("[DEBUG] fingerprint_manager: applied fingerprints %v", appliedFingerprints) + return nil +} diff --git a/client/fingerprint_manager_test.go b/client/fingerprint_manager_test.go new file mode 100644 index 000000000..a0f0c600d --- /dev/null +++ b/client/fingerprint_manager_test.go @@ -0,0 +1,139 @@ +package client + +import ( + "fmt" + "testing" + + "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/client/driver" + "github.com/hashicorp/nomad/nomad/structs" + "github.com/hashicorp/nomad/testutil" + "github.com/stretchr/testify/require" +) + +// test that the driver manager updates a node when its attributes change +func TestFingerprintManager_Fingerprint_MockDriver(t *testing.T) { + if _, ok := driver.BuiltinDrivers["mock_driver"]; !ok { + t.Skip(`test requires mock_driver; run with "-tags nomad_test"`) + } + t.Parallel() + require := require.New(t) + + node := &structs.Node{ + Attributes: make(map[string]string, 0), + } + mockConfig := &config.Config{ + Node: node, + } + c := &Client{ + config: mockConfig, + } + getConfig := func() *config.Config { + return mockConfig + } + + fm := FingerprintManager{ + getConfig: getConfig, + node: node, + client: c, + logger: testLogger(), + } + + // test setting up a mock driver + drivers := []string{"mock_driver"} + err := fm.SetupDrivers(drivers) + require.Nil(err) + + require.NotEqual("", node.Attributes["driver.mock_driver"]) +} + +func TestFingerprintManager_Fingerprint_RawExec(t *testing.T) { + t.Parallel() + require := require.New(t) + + node := &structs.Node{ + Attributes: make(map[string]string, 0), + } + mockConfig := &config.Config{ + Node: node, + Options: map[string]string{ + "driver.raw_exec.enable": "true", + }, + } + c := &Client{ + config: mockConfig, + } + getConfig := func() *config.Config { + return mockConfig + } + + fm := FingerprintManager{ + getConfig: getConfig, + node: node, + client: c, + logger: testLogger(), + } + + // test setting up a mock driver + drivers := []string{"raw_exec"} + err := fm.SetupDrivers(drivers) + require.Nil(err) + + require.NotEqual("", node.Attributes["driver.raw_exec"]) +} + +func TestFingerprintManager_Fingerprint_Periodic(t *testing.T) { + t.Parallel() + require := require.New(t) + + node := &structs.Node{ + Attributes: make(map[string]string, 0), + } + mockConfig := &config.Config{ + Node: node, + Options: map[string]string{ + "test.shutdown_periodic_after": "true", + "test.shutdown_periodic_duration": "3", + }, + } + c := &Client{ + config: mockConfig, + } + getConfig := func() *config.Config { + return mockConfig + } + + fm := FingerprintManager{ + getConfig: getConfig, + node: node, + client: c, + logger: testLogger(), + } + + // test setting up a mock driver + drivers := []string{"mock_driver"} + err := fm.SetupDrivers(drivers) + require.Nil(err) + + // Ensure the mock driver is registered on the client + testutil.WaitForResult(func() (bool, error) { + mockDriverStatus := node.Attributes["driver.mock_driver"] + 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["driver.mock_driver"] + 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) + }) +}