~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/network/utils.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
        "os"
 
9
        "strings"
 
10
 
 
11
        "github.com/juju/errors"
 
12
)
 
13
 
 
14
// DNSConfig holds a list of DNS nameserver addresses and default search
 
15
// domains.
 
16
type DNSConfig struct {
 
17
        Nameservers   []Address
 
18
        SearchDomains []string
 
19
}
 
20
 
 
21
// ParseResolvConf parses a resolv.conf(5) file at the given path (usually
 
22
// "/etc/resolv.conf"), if present. Returns the values of any 'nameserver'
 
23
// stanzas, and the last 'search' stanza found. Values in the result will appear
 
24
// in the order found, including duplicates. Parsing errors will be returned in
 
25
// these cases:
 
26
//
 
27
// 1. if a 'nameserver' or 'search' without a value is found;
 
28
// 2. 'nameserver' with more than one value (trailing comments starting with '#'
 
29
//    or ';' after the value are allowed).
 
30
// 3. if any value containing '#' or ';' (e.g. 'nameserver 8.8.8.8#bad'), because
 
31
//    values and comments following them must be separated by whitespace.
 
32
//
 
33
// No error is returned if the file is missing. See resolv.conf(5) man page for
 
34
// details.
 
35
func ParseResolvConf(path string) (*DNSConfig, error) {
 
36
        file, err := os.Open(path)
 
37
        if os.IsNotExist(err) {
 
38
                logger.Debugf("%q does not exist - not parsing", path)
 
39
                return nil, nil
 
40
        } else if err != nil {
 
41
                return nil, errors.Trace(err)
 
42
        }
 
43
        defer file.Close()
 
44
 
 
45
        var (
 
46
                nameservers   []string
 
47
                searchDomains []string
 
48
        )
 
49
        scanner := bufio.NewScanner(file)
 
50
        lineNum := 0
 
51
        for scanner.Scan() {
 
52
                line := scanner.Text()
 
53
                lineNum++
 
54
 
 
55
                values, err := parseResolvStanza(line, "nameserver")
 
56
                if err != nil {
 
57
                        return nil, errors.Annotatef(err, "parsing %q, line %d", path, lineNum)
 
58
                }
 
59
 
 
60
                if numValues := len(values); numValues > 1 {
 
61
                        return nil, errors.Errorf(
 
62
                                "parsing %q, line %d: one value expected for \"nameserver\", got %d",
 
63
                                path, lineNum, numValues,
 
64
                        )
 
65
                } else if numValues == 1 {
 
66
                        nameservers = append(nameservers, values[0])
 
67
                        continue
 
68
                }
 
69
 
 
70
                values, err = parseResolvStanza(line, "search")
 
71
                if err != nil {
 
72
                        return nil, errors.Annotatef(err, "parsing %q, line %d", path, lineNum)
 
73
                }
 
74
 
 
75
                if len(values) > 0 {
 
76
                        // Last 'search' found wins.
 
77
                        searchDomains = values
 
78
                }
 
79
        }
 
80
 
 
81
        if err := scanner.Err(); err != nil {
 
82
                return nil, errors.Annotatef(err, "reading %q", path)
 
83
        }
 
84
 
 
85
        return &DNSConfig{
 
86
                Nameservers:   NewAddresses(nameservers...),
 
87
                SearchDomains: searchDomains,
 
88
        }, nil
 
89
}
 
90
 
 
91
// parseResolvStanza parses a single line from a resolv.conf(5) file, beginning
 
92
// with the given stanza ('nameserver' or 'search' ). If the line does not
 
93
// contain the stanza, no results and no error is returned. Leading and trailing
 
94
// whitespace is removed first, then lines starting with ";" or "#" are treated
 
95
// as comments.
 
96
//
 
97
// Examples:
 
98
// parseResolvStanza(`   # nothing ;to see here`, "doesn't matter")
 
99
// will return (nil, nil) - comments and whitespace are ignored, nothing left.
 
100
//
 
101
// parseResolvStanza(`   nameserver    ns1.example.com   # preferred`, "nameserver")
 
102
// will return ([]string{"ns1.example.com"}, nil).
 
103
//
 
104
// parseResolvStanza(`search ;; bad: no value`, "search")
 
105
// will return (nil, err: `"search": required value(s) missing`)
 
106
//
 
107
// parseResolvStanza(`search foo bar foo foo.bar bar.foo ;; try all`, "search")
 
108
// will return ([]string("foo", "bar", "foo", "foo.bar", "bar.foo"}, nil)
 
109
//
 
110
// parseResolvStanza(`search foo#bad comment`, "nameserver")
 
111
// will return (nil, nil) - line does not start with "nameserver".
 
112
//
 
113
// parseResolvStanza(`search foo#bad comment`, "search")
 
114
// will return (nil, err: `"search": invalid value "foo#bad"`) - no whitespace
 
115
// between the value "foo" and the following comment "#bad comment".
 
116
func parseResolvStanza(line, stanza string) ([]string, error) {
 
117
        const commentChars = ";#"
 
118
        isComment := func(s string) bool {
 
119
                return strings.IndexAny(s, commentChars) == 0
 
120
        }
 
121
 
 
122
        line = strings.TrimSpace(line)
 
123
        fields := strings.Fields(line)
 
124
        noFields := len(fields) == 0 // line contains only whitespace
 
125
 
 
126
        if isComment(line) || noFields || fields[0] != stanza {
 
127
                // Lines starting with ';' or '#' are comments and are ignored. Empty
 
128
                // lines and those not starting with stanza are ignored.
 
129
                return nil, nil
 
130
        }
 
131
 
 
132
        // Mostly for convenience, comments starting with ';' or '#' after a value
 
133
        // are allowed and ignored, assuming there's whitespace between the value
 
134
        // and the comment (e.g. 'search foo #bar' is OK, but 'search foo#bar'
 
135
        // isn't).
 
136
        var parsedValues []string
 
137
        rawValues := fields[1:] // skip the stanza itself
 
138
        for _, value := range rawValues {
 
139
                if isComment(value) {
 
140
                        // We're done parsing as the rest of the line is still part of the
 
141
                        // same comment.
 
142
                        break
 
143
                }
 
144
 
 
145
                if strings.ContainsAny(value, commentChars) {
 
146
                        // This will catch cases like 'nameserver 8.8.8.8#foo', because
 
147
                        // fields[1] will be '8.8.8.8#foo'.
 
148
                        return nil, errors.Errorf("%q: invalid value %q", stanza, value)
 
149
                }
 
150
 
 
151
                parsedValues = append(parsedValues, value)
 
152
        }
 
153
 
 
154
        // resolv.conf(5) states that to be recognized as valid, the line must begin
 
155
        // with the stanza, followed by whitespace, then at least one value (for
 
156
        // 'nameserver', more values separated by whitespace are allowed for
 
157
        // 'search').
 
158
        if len(parsedValues) == 0 {
 
159
                return nil, errors.Errorf("%q: required value(s) missing", stanza)
 
160
        }
 
161
 
 
162
        return parsedValues, nil
 
163
}