~harlowja/cloud-init/cloud-init-net-refactor

« back to all changes in this revision

Viewing changes to cloudinit/net/cmdline.py

  • Committer: Joshua Harlow
  • Date: 2016-05-19 21:26:30 UTC
  • mfrom: (1215.1.18 cloud-init)
  • Revision ID: harlowja@gmail.com-20160519212630-b2l2fsshopo50fdk
RemergeĀ againstĀ head/master

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#   Copyright (C) 2013-2014 Canonical Ltd.
 
2
#
 
3
#   Author: Scott Moser <scott.moser@canonical.com>
 
4
#   Author: Blake Rouse <blake.rouse@canonical.com>
 
5
#
 
6
#   Curtin is free software: you can redistribute it and/or modify it under
 
7
#   the terms of the GNU Affero General Public License as published by the
 
8
#   Free Software Foundation, either version 3 of the License, or (at your
 
9
#   option) any later version.
 
10
#
 
11
#   Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
 
12
#   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 
13
#   FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for
 
14
#   more details.
 
15
#
 
16
#   You should have received a copy of the GNU Affero General Public License
 
17
#   along with Curtin.  If not, see <http://www.gnu.org/licenses/>.
 
18
 
 
19
import base64
 
20
import glob
 
21
import gzip
 
22
import io
 
23
import shlex
 
24
import sys
 
25
 
 
26
import six
 
27
 
 
28
from cloudinit.net import get_devicelist
 
29
from cloudinit.net import sys_netdev_info
 
30
 
 
31
from cloudinit import util
 
32
 
 
33
PY26 = sys.version_info[0:2] == (2, 6)
 
34
 
 
35
 
 
36
def _shlex_split(blob):
 
37
    if PY26 and isinstance(blob, six.text_type):
 
38
        # Older versions don't support unicode input
 
39
        blob = blob.encode("utf8")
 
40
    return shlex.split(blob)
 
41
 
 
42
 
 
43
def _load_shell_content(content, add_empty=False, empty_val=None):
 
44
    """Given shell like syntax (key=value\nkey2=value2\n) in content
 
45
       return the data in dictionary form.  If 'add_empty' is True
 
46
       then add entries in to the returned dictionary for 'VAR='
 
47
       variables.  Set their value to empty_val."""
 
48
    data = {}
 
49
    for line in _shlex_split(content):
 
50
        try:
 
51
            key, value = line.split("=", 1)
 
52
        except ValueError:
 
53
            # Unsplittable line, skip it...
 
54
            pass
 
55
        else:
 
56
            if not value:
 
57
                value = empty_val
 
58
            if add_empty or value:
 
59
                data[key] = value
 
60
    return data
 
61
 
 
62
 
 
63
def _klibc_to_config_entry(content, mac_addrs=None):
 
64
    """Convert a klibc writtent shell content file to a 'config' entry
 
65
    When ip= is seen on the kernel command line in debian initramfs
 
66
    and networking is brought up, ipconfig will populate
 
67
    /run/net-<name>.cfg.
 
68
 
 
69
    The files are shell style syntax, and examples are in the tests
 
70
    provided here.  There is no good documentation on this unfortunately.
 
71
 
 
72
    DEVICE=<name> is expected/required and PROTO should indicate if
 
73
    this is 'static' or 'dhcp'.
 
74
    """
 
75
 
 
76
    if mac_addrs is None:
 
77
        mac_addrs = {}
 
78
 
 
79
    data = _load_shell_content(content)
 
80
    try:
 
81
        name = data['DEVICE']
 
82
    except KeyError:
 
83
        raise ValueError("no 'DEVICE' entry in data")
 
84
 
 
85
    # ipconfig on precise does not write PROTO
 
86
    proto = data.get('PROTO')
 
87
    if not proto:
 
88
        if data.get('filename'):
 
89
            proto = 'dhcp'
 
90
        else:
 
91
            proto = 'static'
 
92
 
 
93
    if proto not in ('static', 'dhcp'):
 
94
        raise ValueError("Unexpected value for PROTO: %s" % proto)
 
95
 
 
96
    iface = {
 
97
        'type': 'physical',
 
98
        'name': name,
 
99
        'subnets': [],
 
100
    }
 
101
 
 
102
    if name in mac_addrs:
 
103
        iface['mac_address'] = mac_addrs[name]
 
104
 
 
105
    # originally believed there might be IPV6* values
 
106
    for v, pre in (('ipv4', 'IPV4'),):
 
107
        # if no IPV4ADDR or IPV6ADDR, then go on.
 
108
        if pre + "ADDR" not in data:
 
109
            continue
 
110
        subnet = {'type': proto, 'control': 'manual'}
 
111
 
 
112
        # these fields go right on the subnet
 
113
        for key in ('NETMASK', 'BROADCAST', 'GATEWAY'):
 
114
            if pre + key in data:
 
115
                subnet[key.lower()] = data[pre + key]
 
116
 
 
117
        dns = []
 
118
        # handle IPV4DNS0 or IPV6DNS0
 
119
        for nskey in ('DNS0', 'DNS1'):
 
120
            ns = data.get(pre + nskey)
 
121
            # verify it has something other than 0.0.0.0 (or ipv6)
 
122
            if ns and len(ns.strip(":.0")):
 
123
                dns.append(data[pre + nskey])
 
124
        if dns:
 
125
            subnet['dns_nameservers'] = dns
 
126
            # add search to both ipv4 and ipv6, as it has no namespace
 
127
            search = data.get('DOMAINSEARCH')
 
128
            if search:
 
129
                if ',' in search:
 
130
                    subnet['dns_search'] = search.split(",")
 
131
                else:
 
132
                    subnet['dns_search'] = search.split()
 
133
 
 
134
        iface['subnets'].append(subnet)
 
135
 
 
136
    return name, iface
 
137
 
 
138
 
 
139
def config_from_klibc_net_cfg(files=None, mac_addrs=None):
 
140
    if files is None:
 
141
        files = glob.glob('/run/net*.conf')
 
142
 
 
143
    entries = []
 
144
    names = {}
 
145
    for cfg_file in files:
 
146
        name, entry = _klibc_to_config_entry(util.load_file(cfg_file),
 
147
                                             mac_addrs=mac_addrs)
 
148
        if name in names:
 
149
            raise ValueError(
 
150
                "device '%s' defined multiple times: %s and %s" % (
 
151
                    name, names[name], cfg_file))
 
152
 
 
153
        names[name] = cfg_file
 
154
        entries.append(entry)
 
155
    return {'config': entries, 'version': 1}
 
156
 
 
157
 
 
158
def _decomp_gzip(blob, strict=True):
 
159
    # decompress blob. raise exception if not compressed unless strict=False.
 
160
    with io.BytesIO(blob) as iobuf:
 
161
        gzfp = None
 
162
        try:
 
163
            gzfp = gzip.GzipFile(mode="rb", fileobj=iobuf)
 
164
            return gzfp.read()
 
165
        except IOError:
 
166
            if strict:
 
167
                raise
 
168
            return blob
 
169
        finally:
 
170
            if gzfp:
 
171
                gzfp.close()
 
172
 
 
173
 
 
174
def _b64dgz(b64str, gzipped="try"):
 
175
    # decode a base64 string.  If gzipped is true, transparently uncompresss
 
176
    # if gzipped is 'try', then try gunzip, returning the original on fail.
 
177
    try:
 
178
        blob = base64.b64decode(b64str)
 
179
    except TypeError:
 
180
        raise ValueError("Invalid base64 text: %s" % b64str)
 
181
 
 
182
    if not gzipped:
 
183
        return blob
 
184
 
 
185
    return _decomp_gzip(blob, strict=gzipped != "try")
 
186
 
 
187
 
 
188
def read_kernel_cmdline_config(files=None, mac_addrs=None, cmdline=None):
 
189
    if cmdline is None:
 
190
        cmdline = util.get_cmdline()
 
191
 
 
192
    if 'network-config=' in cmdline:
 
193
        data64 = None
 
194
        for tok in cmdline.split():
 
195
            if tok.startswith("network-config="):
 
196
                data64 = tok.split("=", 1)[1]
 
197
        if data64:
 
198
            return util.load_yaml(_b64dgz(data64))
 
199
 
 
200
    if 'ip=' not in cmdline:
 
201
        return None
 
202
 
 
203
    if mac_addrs is None:
 
204
        mac_addrs = dict((k, sys_netdev_info(k, 'address'))
 
205
                         for k in get_devicelist())
 
206
 
 
207
    return config_from_klibc_net_cfg(files=files, mac_addrs=mac_addrs)