open-nomad/client/fingerprint/network_unix.go

221 lines
6.0 KiB
Go

// +build linux darwin
package fingerprint
import (
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"os/exec"
"regexp"
"strconv"
"strings"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/nomad/structs"
)
// NetworkFingerprint is used to fingerprint the Network capabilities of a node
type NetworkFingerprint struct {
logger *log.Logger
}
// NewNetworkFingerprinter returns a new NetworkFingerprinter with the given
// logger
func NewNetworkFingerprinter(logger *log.Logger) Fingerprint {
f := &NetworkFingerprint{logger: logger}
return f
}
func (f *NetworkFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) {
// newNetwork is populated and addded to the Nodes resources
newNetwork := &structs.NetworkResource{}
var ip string
interfaces, err := f.findInterfaces(cfg.NetworkInterface)
if err != nil {
return false, fmt.Errorf("Error while detecting network interface during fingerprinting: %v", err)
}
if len(interfaces) == 0 {
return false, errors.New("No network interfaces were detected")
}
// Use the first interface that we have detected.
intf := interfaces[0]
if ip, err = f.ipAddress(intf); err != nil {
return false, fmt.Errorf("Unable to find IP address of interface: %s, err: %v", intf.Name, err)
}
newNetwork.Device = intf.Name
node.Attributes["network.ip-address"] = ip
newNetwork.IP = ip
newNetwork.CIDR = newNetwork.IP + "/32"
f.logger.Println("[DEBUG] fingerprint.network Detected interface ", intf.Name, " with IP ", ip, " during fingerprinting")
if throughput := f.linkSpeed(intf.Name); throughput > 0 {
newNetwork.MBits = throughput
} else {
f.logger.Printf("[DEBUG] fingerprint.network: Unable to read link speed; setting to default %v", cfg.NetworkSpeed)
newNetwork.MBits = cfg.NetworkSpeed
}
if node.Resources == nil {
node.Resources = &structs.Resources{}
}
node.Resources.Networks = append(node.Resources.Networks, newNetwork)
// return true, because we have a network connection
return true, nil
}
// linkSpeed returns link speed in Mb/s, or 0 when unable to determine it.
func (f *NetworkFingerprint) linkSpeed(device string) int {
// Use LookPath to find the ethtool in the systems $PATH
// If it's not found or otherwise errors, LookPath returns and empty string
// and an error we can ignore for our purposes
ethtoolPath, _ := exec.LookPath("ethtool")
if ethtoolPath != "" {
if speed := f.linkSpeedEthtool(ethtoolPath, device); speed > 0 {
return speed
}
}
// Fall back on checking a system file for link speed.
return f.linkSpeedSys(device)
}
// linkSpeedSys parses link speed in Mb/s from /sys.
func (f *NetworkFingerprint) linkSpeedSys(device string) int {
path := fmt.Sprintf("/sys/class/net/%s/speed", device)
// Read contents of the device/speed file
content, err := ioutil.ReadFile(path)
if err != nil {
f.logger.Printf("[WARN] fingerprint.network: Unable to read link speed from %s", path)
return 0
}
lines := strings.Split(string(content), "\n")
mbs, err := strconv.Atoi(lines[0])
if err != nil || mbs <= 0 {
f.logger.Printf("[WARN] fingerprint.network: Unable to parse link speed from %s", path)
return 0
}
return mbs
}
// linkSpeedEthtool determines link speed in Mb/s with 'ethtool'.
func (f *NetworkFingerprint) linkSpeedEthtool(path, device string) int {
outBytes, err := exec.Command(path, device).Output()
if err != nil {
f.logger.Printf("[WARN] fingerprint.network: Error calling ethtool (%s %s): %v", path, device, err)
return 0
}
output := strings.TrimSpace(string(outBytes))
re := regexp.MustCompile("Speed: [0-9]+[a-zA-Z]+/s")
m := re.FindString(output)
if m == "" {
// no matches found, output may be in a different format
f.logger.Printf("[WARN] fingerprint.network: Unable to parse Speed in output of '%s %s'", path, device)
return 0
}
// Split and trim the Mb/s unit from the string output
args := strings.Split(m, ": ")
raw := strings.TrimSuffix(args[1], "Mb/s")
// convert to Mb/s
mbs, err := strconv.Atoi(raw)
if err != nil || mbs <= 0 {
f.logger.Printf("[WARN] fingerprint.network: Unable to parse Mb/s in output of '%s %s'", path, device)
return 0
}
return mbs
}
// Gets the ipv4 addr for a network interface
func (f *NetworkFingerprint) ipAddress(intf *net.Interface) (string, error) {
var addrs []net.Addr
var err error
if addrs, err = intf.Addrs(); err != nil {
return "", err
}
if len(addrs) == 0 {
return "", errors.New(fmt.Sprintf("Interface %s has no IP address", intf.Name))
}
var ipV4 net.IP
for _, addr := range addrs {
var ip net.IP
switch v := (addr).(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip.To4() != nil {
ipV4 = ip
break
}
}
if ipV4 == nil {
return "", fmt.Errorf("Couldn't parse IP address for interface %s with addr %s", intf.Name)
}
return ipV4.String(), nil
}
// Checks if the device is marked UP by the operator
func (f *NetworkFingerprint) isDeviceEnabled(intf *net.Interface) bool {
return intf.Flags&net.FlagUp != 0
}
// Checks if the device has any IP address configured
func (f *NetworkFingerprint) deviceHasIpAddress(intf *net.Interface) bool {
if _, err := f.ipAddress(intf); err == nil {
return true
}
return false
}
func (n *NetworkFingerprint) isDeviceLoopBackOrPointToPoint(intf *net.Interface) bool {
return intf.Flags&(net.FlagLoopback|net.FlagPointToPoint) == 0
}
// Returns interfaces which are routable and marked as UP
// Tries to get the specific interface if the user has specified name
func (f *NetworkFingerprint) findInterfaces(deviceName string) ([]*net.Interface, error) {
var interfaces []*net.Interface
var err error
if deviceName != "" {
if intf, err := net.InterfaceByName(deviceName); err == nil {
interfaces = append(interfaces, intf)
}
return interfaces, err
}
var intfs []net.Interface
if intfs, err = net.Interfaces(); err != nil {
return nil, err
}
for _, intf := range intfs {
if f.isDeviceEnabled(&intf) && !f.isDeviceLoopBackOrPointToPoint(&intf) && f.deviceHasIpAddress(&intf) {
interfaces = append(interfaces, &intf)
}
}
return interfaces, nil
}