github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/network/utils.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package network
     5  
     6  import (
     7  	"bufio"
     8  	"io/ioutil"
     9  	"net"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"github.com/juju/errors"
    15  )
    16  
    17  // DNSConfig holds a list of DNS nameserver addresses and default search
    18  // domains.
    19  type DNSConfig struct {
    20  	Nameservers   []Address
    21  	SearchDomains []string
    22  }
    23  
    24  // ParseResolvConf parses a resolv.conf(5) file at the given path (usually
    25  // "/etc/resolv.conf"), if present. Returns the values of any 'nameserver'
    26  // stanzas, and the last 'search' stanza found. Values in the result will appear
    27  // in the order found, including duplicates. Parsing errors will be returned in
    28  // these cases:
    29  //
    30  // 1. if a 'nameserver' or 'search' without a value is found;
    31  // 2. 'nameserver' with more than one value (trailing comments starting with '#'
    32  //    or ';' after the value are allowed).
    33  // 3. if any value containing '#' or ';' (e.g. 'nameserver 8.8.8.8#bad'), because
    34  //    values and comments following them must be separated by whitespace.
    35  //
    36  // No error is returned if the file is missing. See resolv.conf(5) man page for
    37  // details.
    38  func ParseResolvConf(path string) (*DNSConfig, error) {
    39  	file, err := os.Open(path)
    40  	if os.IsNotExist(err) {
    41  		logger.Debugf("%q does not exist - not parsing", path)
    42  		return nil, nil
    43  	} else if err != nil {
    44  		return nil, errors.Trace(err)
    45  	}
    46  	defer file.Close()
    47  
    48  	var (
    49  		nameservers   []string
    50  		searchDomains []string
    51  	)
    52  	scanner := bufio.NewScanner(file)
    53  	lineNum := 0
    54  	for scanner.Scan() {
    55  		line := scanner.Text()
    56  		lineNum++
    57  
    58  		values, err := parseResolvStanza(line, "nameserver")
    59  		if err != nil {
    60  			return nil, errors.Annotatef(err, "parsing %q, line %d", path, lineNum)
    61  		}
    62  
    63  		if numValues := len(values); numValues > 1 {
    64  			return nil, errors.Errorf(
    65  				"parsing %q, line %d: one value expected for \"nameserver\", got %d",
    66  				path, lineNum, numValues,
    67  			)
    68  		} else if numValues == 1 {
    69  			nameservers = append(nameservers, values[0])
    70  			continue
    71  		}
    72  
    73  		values, err = parseResolvStanza(line, "search")
    74  		if err != nil {
    75  			return nil, errors.Annotatef(err, "parsing %q, line %d", path, lineNum)
    76  		}
    77  
    78  		if len(values) > 0 {
    79  			// Last 'search' found wins.
    80  			searchDomains = values
    81  		}
    82  	}
    83  
    84  	if err := scanner.Err(); err != nil {
    85  		return nil, errors.Annotatef(err, "reading %q", path)
    86  	}
    87  
    88  	return &DNSConfig{
    89  		Nameservers:   NewAddresses(nameservers...),
    90  		SearchDomains: searchDomains,
    91  	}, nil
    92  }
    93  
    94  // parseResolvStanza parses a single line from a resolv.conf(5) file, beginning
    95  // with the given stanza ('nameserver' or 'search' ). If the line does not
    96  // contain the stanza, no results and no error is returned. Leading and trailing
    97  // whitespace is removed first, then lines starting with ";" or "#" are treated
    98  // as comments.
    99  //
   100  // Examples:
   101  // parseResolvStanza(`   # nothing ;to see here`, "doesn't matter")
   102  // will return (nil, nil) - comments and whitespace are ignored, nothing left.
   103  //
   104  // parseResolvStanza(`   nameserver    ns1.example.com   # preferred`, "nameserver")
   105  // will return ([]string{"ns1.example.com"}, nil).
   106  //
   107  // parseResolvStanza(`search ;; bad: no value`, "search")
   108  // will return (nil, err: `"search": required value(s) missing`)
   109  //
   110  // parseResolvStanza(`search foo bar foo foo.bar bar.foo ;; try all`, "search")
   111  // will return ([]string("foo", "bar", "foo", "foo.bar", "bar.foo"}, nil)
   112  //
   113  // parseResolvStanza(`search foo#bad comment`, "nameserver")
   114  // will return (nil, nil) - line does not start with "nameserver".
   115  //
   116  // parseResolvStanza(`search foo#bad comment`, "search")
   117  // will return (nil, err: `"search": invalid value "foo#bad"`) - no whitespace
   118  // between the value "foo" and the following comment "#bad comment".
   119  func parseResolvStanza(line, stanza string) ([]string, error) {
   120  	const commentChars = ";#"
   121  	isComment := func(s string) bool {
   122  		return strings.IndexAny(s, commentChars) == 0
   123  	}
   124  
   125  	line = strings.TrimSpace(line)
   126  	fields := strings.Fields(line)
   127  	noFields := len(fields) == 0 // line contains only whitespace
   128  
   129  	if isComment(line) || noFields || fields[0] != stanza {
   130  		// Lines starting with ';' or '#' are comments and are ignored. Empty
   131  		// lines and those not starting with stanza are ignored.
   132  		return nil, nil
   133  	}
   134  
   135  	// Mostly for convenience, comments starting with ';' or '#' after a value
   136  	// are allowed and ignored, assuming there's whitespace between the value
   137  	// and the comment (e.g. 'search foo #bar' is OK, but 'search foo#bar'
   138  	// isn't).
   139  	var parsedValues []string
   140  	rawValues := fields[1:] // skip the stanza itself
   141  	for _, value := range rawValues {
   142  		if isComment(value) {
   143  			// We're done parsing as the rest of the line is still part of the
   144  			// same comment.
   145  			break
   146  		}
   147  
   148  		if strings.ContainsAny(value, commentChars) {
   149  			// This will catch cases like 'nameserver 8.8.8.8#foo', because
   150  			// fields[1] will be '8.8.8.8#foo'.
   151  			return nil, errors.Errorf("%q: invalid value %q", stanza, value)
   152  		}
   153  
   154  		parsedValues = append(parsedValues, value)
   155  	}
   156  
   157  	// resolv.conf(5) states that to be recognized as valid, the line must begin
   158  	// with the stanza, followed by whitespace, then at least one value (for
   159  	// 'nameserver', more values separated by whitespace are allowed for
   160  	// 'search').
   161  	if len(parsedValues) == 0 {
   162  		return nil, errors.Errorf("%q: required value(s) missing", stanza)
   163  	}
   164  
   165  	return parsedValues, nil
   166  }
   167  
   168  var netListen = net.Listen
   169  
   170  // SupportsIPv6 reports whether the platform supports IPv6 networking
   171  // functionality.
   172  //
   173  // Source: https://github.com/golang/net/blob/master/internal/nettest/stack.go
   174  func SupportsIPv6() bool {
   175  	ln, err := netListen("tcp6", "[::1]:0")
   176  	if err != nil {
   177  		return false
   178  	}
   179  	ln.Close()
   180  	return true
   181  }
   182  
   183  // SysClassNetRoot is the full Linux SYSFS path containing information about
   184  // each network interface on the system. Used as argument to
   185  // ParseInterfaceType().
   186  const SysClassNetPath = "/sys/class/net"
   187  
   188  // ParseInterfaceType parses the DEVTYPE attribute from the Linux kernel
   189  // userspace SYSFS location "<sysPath/<interfaceName>/uevent" and returns it as
   190  // InterfaceType. SysClassNetPath should be passed as sysPath. Returns
   191  // UnknownInterface if the type cannot be reliably determined for any reason.
   192  //
   193  // Example call: network.ParseInterfaceType(network.SysClassNetPath, "br-eth1")
   194  func ParseInterfaceType(sysPath, interfaceName string) InterfaceType {
   195  	const deviceType = "DEVTYPE="
   196  	location := filepath.Join(sysPath, interfaceName, "uevent")
   197  
   198  	data, err := ioutil.ReadFile(location)
   199  	if err != nil {
   200  		logger.Debugf("ignoring error reading %q: %v", location, err)
   201  		return UnknownInterface
   202  	}
   203  
   204  	devtype := ""
   205  	lines := strings.Fields(string(data))
   206  	for _, line := range lines {
   207  		if !strings.HasPrefix(line, deviceType) {
   208  			continue
   209  		}
   210  
   211  		devtype = strings.TrimPrefix(line, deviceType)
   212  		switch devtype {
   213  		case "bridge":
   214  			return BridgeInterface
   215  		case "vlan":
   216  			return VLAN_8021QInterface
   217  		case "bond":
   218  			return BondInterface
   219  		case "":
   220  			// DEVTYPE is not present for some types, like Ethernet and loopback
   221  			// interfaces, so if missing do not try to guess.
   222  			break
   223  		}
   224  	}
   225  
   226  	return UnknownInterface
   227  }
   228  
   229  // GetBridgePorts extracts and returns the names of all interfaces configured as
   230  // ports of the given bridgeName from the Linux kernel userspace SYSFS location
   231  // "<sysPath/<bridgeName>/brif/*". SysClassNetPath should be passed as sysPath.
   232  // Returns an empty result if the ports cannot be determined reliably for any
   233  // reason, or if there are no configured ports for the bridge.
   234  //
   235  // Example call: network.GetBridgePorts(network.SysClassNetPath, "br-eth1")
   236  func GetBridgePorts(sysPath, bridgeName string) []string {
   237  	portsGlobPath := filepath.Join(sysPath, bridgeName, "brif", "*")
   238  	// Glob ignores I/O errors and can only return ErrBadPattern, which we treat
   239  	// as no results, but for debugging we're still logging the error.
   240  	paths, err := filepath.Glob(portsGlobPath)
   241  	if err != nil {
   242  		logger.Debugf("ignoring error traversing path %q: %v", portsGlobPath, err)
   243  	}
   244  
   245  	if len(paths) == 0 {
   246  		return nil
   247  	}
   248  
   249  	// We need to convert full paths like /sys/class/net/br-eth0/brif/eth0 to
   250  	// just names.
   251  	names := make([]string, len(paths))
   252  	for i := range paths {
   253  		names[i] = filepath.Base(paths[i])
   254  	}
   255  	return names
   256  }