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 }