Merge pull request #4 from hashicorp/f-storage-fingerprint

Add storage fingerprinter
This commit is contained in:
Chris Bednarski 2015-08-27 12:43:18 -07:00
commit 78767f0ff4
10 changed files with 230 additions and 18 deletions

View File

@ -188,7 +188,7 @@ func (c *Client) pickServer() (net.Addr, error) {
c.lastRPCTime = time.Now()
return addr, nil
}
c.logger.Printf("[WARN] client: failed to resolve '%s': %v", err)
c.logger.Printf("[WARN] client: failed to resolve '%s': %s", servers[i], err)
}
// Bail if we reach this point

View File

@ -15,7 +15,7 @@ type ArchFingerprint struct {
// NewArchFingerprint is used to create an OS fingerprint
func NewArchFingerprint(logger *log.Logger) Fingerprint {
f := &ArchFingerprint{logger}
f := &ArchFingerprint{logger: logger}
return f
}

View File

@ -1,10 +0,0 @@
package fingerprint
import (
"log"
"os"
)
func testLogger() *log.Logger {
return log.New(os.Stderr, "", log.LstdFlags)
}

View File

@ -16,7 +16,7 @@ type CPUFingerprint struct {
// NewCPUFingerprint is used to create a CPU fingerprint
func NewCPUFingerprint(logger *log.Logger) Fingerprint {
f := &CPUFingerprint{logger}
f := &CPUFingerprint{logger: logger}
return f
}

View File

@ -0,0 +1,48 @@
package fingerprint
// This file contains helper methods for testing fingerprinters
import (
"log"
"os"
"testing"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/nomad/structs"
)
func testLogger() *log.Logger {
return log.New(os.Stderr, "", log.LstdFlags)
}
func assertFingerprintOK(t *testing.T, fp Fingerprint, node *structs.Node) {
ok, err := fp.Fingerprint(new(config.Config), node)
if err != nil {
t.Fatalf("Failed to fingerprint: %s", err)
}
if !ok {
t.Fatalf("Failed to apply node attributes")
}
}
func assertNodeAttributeContains(t *testing.T, node *structs.Node, attribute string) {
actual, found := node.Attributes[attribute]
if !found {
t.Errorf("Expected to find `%s`\n\n[DEBUG] %#v", attribute, node)
return
}
if actual == "" {
t.Errorf("Expected non-empty value for `%s`\n\n[DEBUG] %#v", attribute, node)
}
}
func assertNodeAttributeEquals(t *testing.T, node *structs.Node, attribute string, expected string) {
actual, found := node.Attributes[attribute]
if !found {
t.Errorf("Expected to find `%s`; unable to check value\n\n[DEBUG] %#v", attribute, node)
return
}
if expected != actual {
t.Errorf("Expected `%s` to be `%s`, found `%s`\n\n[DEBUG] %#v", attribute, expected, actual, node)
}
}

View File

@ -15,7 +15,7 @@ type HostFingerprint struct {
// NewHostFingerprint is used to create a Host fingerprint
func NewHostFingerprint(logger *log.Logger) Fingerprint {
f := &HostFingerprint{logger}
f := &HostFingerprint{logger: logger}
return f
}

View File

@ -22,8 +22,6 @@ func TestHostFingerprint(t *testing.T) {
// Host info
for _, key := range []string{"os.name", "os.version", "hostname", "kernel.name"} {
if node.Attributes[key] == "" {
t.Fatalf("Missing (%s) in Host Info attribute check", key)
}
assertNodeAttributeContains(t, node, key)
}
}

View File

@ -15,7 +15,7 @@ type OSFingerprint struct {
// NewOSFingerprint is used to create an OS fingerprint
func NewOSFingerprint(logger *log.Logger) Fingerprint {
f := &OSFingerprint{logger}
f := &OSFingerprint{logger: logger}
return f
}

View File

@ -0,0 +1,122 @@
package fingerprint
import (
"fmt"
"log"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/nomad/structs"
)
// StorageFingerprint is used to measure the amount of storage free for
// applications that the Nomad agent will run on this machine.
type StorageFingerprint struct {
logger *log.Logger
}
var (
reWindowsTotalSpace = regexp.MustCompile("Total # of bytes\\s+: (\\d+)")
reWindowsFreeSpace = regexp.MustCompile("Total # of free bytes\\s+: (\\d+)")
)
func NewStorageFingerprint(logger *log.Logger) Fingerprint {
fp := &StorageFingerprint{logger: logger}
return fp
}
func (f *StorageFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) {
// Initialize these to empty defaults
node.Attributes["storage.volume"] = ""
node.Attributes["storage.bytestotal"] = ""
node.Attributes["storage.bytesfree"] = ""
if node.Resources == nil {
node.Resources = &structs.Resources{}
}
if runtime.GOOS == "windows" {
path, err := filepath.Abs(cfg.AllocDir)
if err != nil {
return false, fmt.Errorf("Failed to detect volume for storage directory %s: %s", cfg.AllocDir, err)
}
volume := filepath.VolumeName(path)
node.Attributes["storage.volume"] = volume
out, err := exec.Command("fsutil", "volume", "diskfree", volume).Output()
if err != nil {
return false, fmt.Errorf("Failed to inspect free space from volume %s: %s", volume, err)
}
outstring := string(out)
totalMatches := reWindowsTotalSpace.FindStringSubmatch(outstring)
if len(totalMatches) == 2 {
node.Attributes["storage.bytestotal"] = totalMatches[1]
total, err := strconv.ParseInt(totalMatches[1], 10, 64)
if err != nil {
return false, fmt.Errorf("Failed to parse storage.bytestotal in bytes: %s", err)
}
// Convert from bytes to to MB
node.Resources.DiskMB = int(total / 1024 / 1024)
} else {
return false, fmt.Errorf("Failed to parse output from fsutil")
}
freeMatches := reWindowsFreeSpace.FindStringSubmatch(outstring)
if len(freeMatches) == 2 {
node.Attributes["storage.bytesfree"] = freeMatches[1]
_, err := strconv.ParseInt(freeMatches[1], 10, 64)
if err != nil {
return false, fmt.Errorf("Failed to parse storage.bytesfree in bytes: %s", err)
}
} else {
return false, fmt.Errorf("Failed to parse output from fsutil")
}
} else {
path, err := filepath.Abs(cfg.AllocDir)
if err != nil {
return false, fmt.Errorf("Failed to determine absolute path for %s", cfg.AllocDir)
}
// Use -k to standardize the output values between darwin and linux
mountOutput, err := exec.Command("df", "-k", path).Output()
if err != nil {
return false, fmt.Errorf("Failed to determine mount point for %s", path)
}
// Output looks something like:
// Filesystem 1024-blocks Used Available Capacity iused ifree %iused Mounted on
// /dev/disk1 487385240 423722532 63406708 87% 105994631 15851677 87% /
// [0] volume [1] capacity [2] SKIP [3] free
lines := strings.Split(string(mountOutput), "\n")
if len(lines) < 2 {
return false, fmt.Errorf("Failed to parse `df` output; expected 2 lines")
}
fields := strings.Fields(lines[1])
if len(fields) != 9 {
return false, fmt.Errorf("Failed to parse `df` output; expected 9 columns")
}
node.Attributes["storage.volume"] = fields[0]
total, err := strconv.ParseInt(fields[1], 10, 64)
if err != nil {
return false, fmt.Errorf("Failed to parse storage.bytestotal size in kilobytes")
}
node.Attributes["storage.bytestotal"] = strconv.FormatInt(total*1024, 10)
free, err := strconv.ParseInt(fields[3], 10, 64)
if err != nil {
return false, fmt.Errorf("Failed to parse storage.bytesfree size in kilobytes")
}
// Convert from KB to MB
node.Resources.DiskMB = int(free / 1024)
// Convert from KB to bytes
node.Attributes["storage.bytesfree"] = strconv.FormatInt(free*1024, 10)
}
return true, nil
}

View File

@ -0,0 +1,54 @@
package fingerprint
import (
"os"
"strconv"
"testing"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/nomad/structs"
)
func TestStorageFingerprint(t *testing.T) {
fp := NewStorageFingerprint(testLogger())
node := &structs.Node{
Attributes: make(map[string]string),
}
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get test working directory: %s", err)
}
cfg := &config.Config{
AllocDir: cwd,
}
ok, err := fp.Fingerprint(cfg, node)
if err != nil {
t.Fatalf("Failed to fingerprint: `%s`", err)
}
if !ok {
t.Fatal("Failed to apply node attributes")
}
assertNodeAttributeContains(t, node, "storage.volume")
assertNodeAttributeContains(t, node, "storage.bytestotal")
assertNodeAttributeContains(t, node, "storage.bytesfree")
total, err := strconv.ParseInt(node.Attributes["storage.bytestotal"], 10, 64)
if err != nil {
t.Fatalf("Failed to parse storage.bytestotal: %s", err)
}
free, err := strconv.ParseInt(node.Attributes["storage.bytesfree"], 10, 64)
if err != nil {
t.Fatalf("Failed to parse storage.bytesfree: %s", err)
}
if free > total {
t.Errorf("storage.bytesfree %d is larger than storage.bytestotal %d", free, total)
}
if node.Resources.DiskMB == 0 {
t.Errorf("Expected node.Resources.DiskMB to be non-zero")
}
}