github.com/kardianos/nomad@v0.1.3-0.20151022182107-b13df73ee850/client/fingerprint/network_unix.go (about)

     1  // +build linux darwin
     2  
     3  package fingerprint
     4  
     5  import (
     6  	"errors"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"log"
    10  	"net"
    11  	"os/exec"
    12  	"regexp"
    13  	"runtime"
    14  	"strconv"
    15  	"strings"
    16  
    17  	"github.com/hashicorp/nomad/client/config"
    18  	"github.com/hashicorp/nomad/nomad/structs"
    19  )
    20  
    21  // NetworkFingerprint is used to fingerprint the Network capabilities of a node
    22  type NetworkFingerprint struct {
    23  	logger *log.Logger
    24  }
    25  
    26  // NewNetworkFingerprinter returns a new NetworkFingerprinter with the given
    27  // logger
    28  func NewNetworkFingerprinter(logger *log.Logger) Fingerprint {
    29  	f := &NetworkFingerprint{logger: logger}
    30  	return f
    31  }
    32  
    33  func (f *NetworkFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) {
    34  	// newNetwork is populated and addded to the Nodes resources
    35  	newNetwork := &structs.NetworkResource{}
    36  
    37  	// eth0 is the default device for Linux, and en0 is default for OS X
    38  	defaultDevice := "eth0"
    39  	if "darwin" == runtime.GOOS {
    40  		defaultDevice = "en0"
    41  	}
    42  	// User-defined override for the default interface
    43  	if cfg.NetworkInterface != "" {
    44  		defaultDevice = cfg.NetworkInterface
    45  	}
    46  
    47  	newNetwork.Device = defaultDevice
    48  
    49  	if ip := f.ipAddress(defaultDevice); ip != "" {
    50  		node.Attributes["network.ip-address"] = ip
    51  		newNetwork.IP = ip
    52  		newNetwork.CIDR = newNetwork.IP + "/32"
    53  	} else {
    54  		return false, fmt.Errorf("Unable to determine IP on network interface %v", defaultDevice)
    55  	}
    56  
    57  	if throughput := f.linkSpeed(defaultDevice); throughput > 0 {
    58  		newNetwork.MBits = throughput
    59  	} else {
    60  		f.logger.Printf("[DEBUG] fingerprint.network: Unable to read link speed; setting to default %v", cfg.NetworkSpeed)
    61  		newNetwork.MBits = cfg.NetworkSpeed
    62  	}
    63  
    64  	if node.Resources == nil {
    65  		node.Resources = &structs.Resources{}
    66  	}
    67  
    68  	node.Resources.Networks = append(node.Resources.Networks, newNetwork)
    69  
    70  	// return true, because we have a network connection
    71  	return true, nil
    72  }
    73  
    74  // linkSpeed returns link speed in Mb/s, or 0 when unable to determine it.
    75  func (f *NetworkFingerprint) linkSpeed(device string) int {
    76  	// Use LookPath to find the ethtool in the systems $PATH
    77  	// If it's not found or otherwise errors, LookPath returns and empty string
    78  	// and an error we can ignore for our purposes
    79  	ethtoolPath, _ := exec.LookPath("ethtool")
    80  	if ethtoolPath != "" {
    81  		if speed := f.linkSpeedEthtool(ethtoolPath, device); speed > 0 {
    82  			return speed
    83  		}
    84  	}
    85  
    86  	// Fall back on checking a system file for link speed.
    87  	return f.linkSpeedSys(device)
    88  }
    89  
    90  // linkSpeedSys parses link speed in Mb/s from /sys.
    91  func (f *NetworkFingerprint) linkSpeedSys(device string) int {
    92  	path := fmt.Sprintf("/sys/class/net/%s/speed", device)
    93  
    94  	// Read contents of the device/speed file
    95  	content, err := ioutil.ReadFile(path)
    96  	if err != nil {
    97  		f.logger.Printf("[WARN] fingerprint.network: Unable to read link speed from %s", path)
    98  		return 0
    99  	}
   100  
   101  	lines := strings.Split(string(content), "\n")
   102  	mbs, err := strconv.Atoi(lines[0])
   103  	if err != nil || mbs <= 0 {
   104  		f.logger.Printf("[WARN] fingerprint.network: Unable to parse link speed from %s", path)
   105  		return 0
   106  	}
   107  
   108  	return mbs
   109  }
   110  
   111  // linkSpeedEthtool determines link speed in Mb/s with 'ethtool'.
   112  func (f *NetworkFingerprint) linkSpeedEthtool(path, device string) int {
   113  	outBytes, err := exec.Command(path, device).Output()
   114  	if err != nil {
   115  		f.logger.Printf("[WARN] fingerprint.network: Error calling ethtool (%s %s): %v", path, device, err)
   116  		return 0
   117  	}
   118  
   119  	output := strings.TrimSpace(string(outBytes))
   120  	re := regexp.MustCompile("Speed: [0-9]+[a-zA-Z]+/s")
   121  	m := re.FindString(output)
   122  	if m == "" {
   123  		// no matches found, output may be in a different format
   124  		f.logger.Printf("[WARN] fingerprint.network: Unable to parse Speed in output of '%s %s'", path, device)
   125  		return 0
   126  	}
   127  
   128  	// Split and trim the Mb/s unit from the string output
   129  	args := strings.Split(m, ": ")
   130  	raw := strings.TrimSuffix(args[1], "Mb/s")
   131  
   132  	// convert to Mb/s
   133  	mbs, err := strconv.Atoi(raw)
   134  	if err != nil || mbs <= 0 {
   135  		f.logger.Printf("[WARN] fingerprint.network: Unable to parse Mb/s in output of '%s %s'", path, device)
   136  		return 0
   137  	}
   138  
   139  	return mbs
   140  }
   141  
   142  // ipAddress returns the first IPv4 address on the configured default interface
   143  // Tries Golang native functions and falls back onto ifconfig
   144  func (f *NetworkFingerprint) ipAddress(device string) string {
   145  	if ip, err := f.nativeIpAddress(device); err == nil {
   146  		return ip
   147  	}
   148  
   149  	return f.ifConfig(device)
   150  }
   151  
   152  func (f *NetworkFingerprint) nativeIpAddress(device string) (string, error) {
   153  	// Find IP address on configured interface
   154  	var ip string
   155  	ifaces, err := net.Interfaces()
   156  	if err != nil {
   157  		return "", errors.New("could not retrieve interface list")
   158  	}
   159  
   160  	// TODO: should we handle IPv6 here? How do we determine precedence?
   161  	for _, i := range ifaces {
   162  		if i.Name != device {
   163  			continue
   164  		}
   165  
   166  		addrs, err := i.Addrs()
   167  		if err != nil {
   168  			return "", errors.New("could not retrieve interface IP addresses")
   169  		}
   170  
   171  		for _, a := range addrs {
   172  			switch v := a.(type) {
   173  			case *net.IPNet:
   174  				if v.IP.To4() != nil {
   175  					ip = v.IP.String()
   176  				}
   177  			case *net.IPAddr:
   178  				if v.IP.To4() != nil {
   179  					ip = v.IP.String()
   180  				}
   181  			}
   182  		}
   183  	}
   184  
   185  	if net.ParseIP(ip) == nil {
   186  		return "", errors.New(fmt.Sprintf("could not parse IP address `%s`", ip))
   187  	}
   188  
   189  	return ip, nil
   190  }
   191  
   192  // ifConfig returns the IP Address for this node according to ifConfig, for the
   193  // specified device.
   194  func (f *NetworkFingerprint) ifConfig(device string) string {
   195  	ifConfigPath, _ := exec.LookPath("ifconfig")
   196  	if ifConfigPath == "" {
   197  		f.logger.Println("[WARN] fingerprint.network: ifconfig not found")
   198  		return ""
   199  	}
   200  
   201  	outBytes, err := exec.Command(ifConfigPath, device).Output()
   202  	if err != nil {
   203  		f.logger.Printf("[WARN] fingerprint.network: Error calling ifconfig (%s %s): %v", ifConfigPath, device, err)
   204  		return ""
   205  	}
   206  
   207  	// Parse out the IP address returned from ifconfig for this device
   208  	// Tested on Ubuntu, the matching part of ifconfig output for eth0 is like
   209  	// so:
   210  	//   inet addr:10.0.2.15  Bcast:10.0.2.255  Mask:255.255.255.0
   211  	// For OS X and en0, we have:
   212  	//  inet 192.168.0.7 netmask 0xffffff00 broadcast 192.168.0.255
   213  	output := strings.TrimSpace(string(outBytes))
   214  
   215  	// re is a regular expression, which can vary based on the OS
   216  	var re *regexp.Regexp
   217  
   218  	if "darwin" == runtime.GOOS {
   219  		re = regexp.MustCompile("inet [0-9].+")
   220  	} else {
   221  		re = regexp.MustCompile("inet addr:[0-9].+")
   222  	}
   223  	args := strings.Split(re.FindString(output), " ")
   224  
   225  	var ip string
   226  	if len(args) > 1 {
   227  		ip = strings.TrimPrefix(args[1], "addr:")
   228  	}
   229  
   230  	// validate what we've sliced out is a valid IP
   231  	if net.ParseIP(ip) == nil {
   232  		f.logger.Printf("[WARN] fingerprint.network: Unable to parse IP in output of '%s %s'", ifConfigPath, device)
   233  		return ""
   234  	}
   235  
   236  	return ip
   237  }