~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/provider/maas/bridgescript.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
// This file is auto generated. Edits will be lost.
 
2
 
 
3
package maas
 
4
 
 
5
//go:generate make -q
 
6
 
 
7
import "path"
 
8
 
 
9
const bridgeScriptName = "add-juju-bridge.py"
 
10
 
 
11
var bridgeScriptPath = path.Join("/var/tmp", bridgeScriptName)
 
12
 
 
13
const bridgeScriptPython = `#!/usr/bin/env python
 
14
 
 
15
# Copyright 2015 Canonical Ltd.
 
16
# Licensed under the AGPLv3, see LICENCE file for details.
 
17
 
 
18
#
 
19
# This file has been and should be formatted using pyfmt(1).
 
20
#
 
21
 
 
22
from __future__ import print_function
 
23
import argparse
 
24
import os
 
25
import re
 
26
import shutil
 
27
import subprocess
 
28
import sys
 
29
 
 
30
# These options are to be removed from a sub-interface and applied to
 
31
# the new bridged interface.
 
32
 
 
33
BRIDGE_ONLY_OPTIONS = {'address', 'gateway', 'netmask', 'dns-nameservers', 'dns-search', 'dns-sortlist'}
 
34
 
 
35
 
 
36
class SeekableIterator(object):
 
37
    """An iterator that supports relative seeking."""
 
38
 
 
39
    def __init__(self, iterable):
 
40
        self.iterable = iterable
 
41
        self.index = 0
 
42
 
 
43
    def __iter__(self):
 
44
        return self
 
45
 
 
46
    def next(self):  # Python 2
 
47
        try:
 
48
            value = self.iterable[self.index]
 
49
            self.index += 1
 
50
            return value
 
51
        except IndexError:
 
52
            raise StopIteration
 
53
 
 
54
    def __next__(self):  # Python 3
 
55
        return self.next()
 
56
 
 
57
    def seek(self, n, relative=False):
 
58
        if relative:
 
59
            self.index += n
 
60
        else:
 
61
            self.index = n
 
62
        if self.index < 0 or self.index >= len(self.iterable):
 
63
            raise IndexError
 
64
 
 
65
 
 
66
class PhysicalInterface(object):
 
67
    """Represents a physical ('auto') interface."""
 
68
 
 
69
    def __init__(self, definition):
 
70
        self.name = definition.split()[1]
 
71
 
 
72
    def __str__(self):
 
73
        return self.name
 
74
 
 
75
 
 
76
class LogicalInterface(object):
 
77
    """Represents a logical ('iface') interface."""
 
78
 
 
79
    def __init__(self, definition, options=None):
 
80
        if not options:
 
81
            options = []
 
82
        _, self.name, self.family, self.method = definition.split()
 
83
        self.options = options
 
84
        self.is_bonded = [x for x in self.options if "bond-" in x]
 
85
        self.is_alias = ":" in self.name
 
86
        self.is_vlan = [x for x in self.options if x.startswith("vlan-raw-device")]
 
87
        self.is_active = self.method == "dhcp" or self.method == "static"
 
88
        self.is_bridged = [x for x in self.options if x.startswith("bridge_ports ")]
 
89
        self.has_auto_stanza = None
 
90
        self.parent = None
 
91
 
 
92
    def __str__(self):
 
93
        return self.name
 
94
 
 
95
    @classmethod
 
96
    def prune_options(cls, options, invalid_options):
 
97
        result = []
 
98
        for o in options:
 
99
            words = o.split()
 
100
            if words[0] not in invalid_options:
 
101
                result.append(o)
 
102
        return result
 
103
 
 
104
    # Returns an ordered set of stanzas to bridge this interface.
 
105
    def bridge(self, prefix, bridge_name):
 
106
        if bridge_name is None:
 
107
            bridge_name = prefix + self.name
 
108
        # Note: the testing order here is significant.
 
109
        if not self.is_active or self.is_bridged:
 
110
            return self._bridge_unchanged()
 
111
        elif self.is_alias:
 
112
            if self.parent and self.parent.iface and (not self.parent.iface.is_active or self.parent.iface.is_bridged):
 
113
                # if we didn't change the parent interface
 
114
                # then we don't change the aliases neither.
 
115
                return self._bridge_unchanged()
 
116
            else:
 
117
                return self._bridge_alias(bridge_name)
 
118
        elif self.is_vlan:
 
119
            return self._bridge_vlan(bridge_name)
 
120
        elif self.is_bonded:
 
121
            return self._bridge_bond(bridge_name)
 
122
        else:
 
123
            return self._bridge_device(bridge_name)
 
124
 
 
125
    def _bridge_device(self, bridge_name):
 
126
        stanzas = []
 
127
        if self.has_auto_stanza:
 
128
            stanzas.append(AutoStanza(self.name))
 
129
        options = self.prune_options(self.options, BRIDGE_ONLY_OPTIONS)
 
130
        stanzas.append(IfaceStanza(self.name, self.family, "manual", options))
 
131
        stanzas.append(AutoStanza(bridge_name))
 
132
        options = list(self.options)
 
133
        options.append("bridge_ports {}".format(self.name))
 
134
        options = self.prune_options(options, ['mtu'])
 
135
        stanzas.append(IfaceStanza(bridge_name, self.family, self.method, options))
 
136
        return stanzas
 
137
 
 
138
    def _bridge_vlan(self, bridge_name):
 
139
        stanzas = []
 
140
        if self.has_auto_stanza:
 
141
            stanzas.append(AutoStanza(self.name))
 
142
        options = self.prune_options(self.options, BRIDGE_ONLY_OPTIONS)
 
143
        stanzas.append(IfaceStanza(self.name, self.family, "manual", options))
 
144
        stanzas.append(AutoStanza(bridge_name))
 
145
        options = list(self.options)
 
146
        options.append("bridge_ports {}".format(self.name))
 
147
        options = self.prune_options(options, ['mtu', 'vlan_id', 'vlan-raw-device'])
 
148
        stanzas.append(IfaceStanza(bridge_name, self.family, self.method, options))
 
149
        return stanzas
 
150
 
 
151
    def _bridge_alias(self, bridge_name):
 
152
        stanzas = []
 
153
        if self.has_auto_stanza:
 
154
            stanzas.append(AutoStanza(bridge_name))
 
155
        stanzas.append(IfaceStanza(bridge_name, self.family, self.method, list(self.options)))
 
156
        return stanzas
 
157
 
 
158
    def _bridge_bond(self, bridge_name):
 
159
        stanzas = []
 
160
        if self.has_auto_stanza:
 
161
            stanzas.append(AutoStanza(self.name))
 
162
        options = self.prune_options(self.options, BRIDGE_ONLY_OPTIONS)
 
163
        stanzas.append(IfaceStanza(self.name, self.family, "manual", options))
 
164
        stanzas.append(AutoStanza(bridge_name))
 
165
        options = [x for x in self.options if not x.startswith("bond")]
 
166
        options = self.prune_options(options, ['mtu'])
 
167
        options.append("bridge_ports {}".format(self.name))
 
168
        stanzas.append(IfaceStanza(bridge_name, self.family, self.method, options))
 
169
        return stanzas
 
170
 
 
171
    def _bridge_unchanged(self):
 
172
        stanzas = []
 
173
        if self.has_auto_stanza:
 
174
            stanzas.append(AutoStanza(self.name))
 
175
        stanzas.append(IfaceStanza(self.name, self.family, self.method, list(self.options)))
 
176
        return stanzas
 
177
 
 
178
 
 
179
class Stanza(object):
 
180
    """Represents one stanza together with all of its options."""
 
181
 
 
182
    def __init__(self, definition, options=None):
 
183
        if not options:
 
184
            options = []
 
185
        self.definition = definition
 
186
        self.options = options
 
187
        self.is_logical_interface = definition.startswith('iface ')
 
188
        self.is_physical_interface = definition.startswith('auto ')
 
189
        self.iface = None
 
190
        self.phy = None
 
191
        if self.is_logical_interface:
 
192
            self.iface = LogicalInterface(definition, self.options)
 
193
        if self.is_physical_interface:
 
194
            self.phy = PhysicalInterface(definition)
 
195
 
 
196
    def __str__(self):
 
197
        return self.definition
 
198
 
 
199
 
 
200
class NetworkInterfaceParser(object):
 
201
    """Parse a network interface file into a set of stanzas."""
 
202
 
 
203
    @classmethod
 
204
    def is_stanza(cls, s):
 
205
        return re.match(r'^(iface|mapping|auto|allow-|source)', s)
 
206
 
 
207
    def __init__(self, filename):
 
208
        self._stanzas = []
 
209
        with open(filename, 'r') as f:
 
210
            lines = f.readlines()
 
211
        line_iterator = SeekableIterator(lines)
 
212
        for line in line_iterator:
 
213
            if self.is_stanza(line):
 
214
                stanza = self._parse_stanza(line, line_iterator)
 
215
                self._stanzas.append(stanza)
 
216
        physical_interfaces = self._physical_interfaces()
 
217
        for s in self._stanzas:
 
218
            if not s.is_logical_interface:
 
219
                continue
 
220
            s.iface.has_auto_stanza = s.iface.name in physical_interfaces
 
221
 
 
222
        self._connect_aliases()
 
223
 
 
224
    def _parse_stanza(self, stanza_line, iterable):
 
225
        stanza_options = []
 
226
        for line in iterable:
 
227
            line = line.strip()
 
228
            if line.startswith('#') or line == "":
 
229
                continue
 
230
            if self.is_stanza(line):
 
231
                iterable.seek(-1, True)
 
232
                break
 
233
            stanza_options.append(line)
 
234
        return Stanza(stanza_line.strip(), stanza_options)
 
235
 
 
236
    def stanzas(self):
 
237
        return [x for x in self._stanzas]
 
238
 
 
239
    def _connect_aliases(self):
 
240
        """Set a reference in the alias interfaces to its related interface"""
 
241
        ifaces = {}
 
242
        aliases = []
 
243
        for stanza in self._stanzas:
 
244
            if stanza.iface is None:
 
245
                continue
 
246
 
 
247
            if stanza.iface.is_alias:
 
248
                aliases.append(stanza)
 
249
            else:
 
250
                ifaces[stanza.iface.name] = stanza
 
251
 
 
252
        for alias in aliases:
 
253
            parent_name = alias.iface.name.split(':')[0]
 
254
            if parent_name in ifaces:
 
255
                alias.iface.parent = ifaces[parent_name]
 
256
 
 
257
    def _physical_interfaces(self):
 
258
        return {x.phy.name: x.phy for x in [y for y in self._stanzas if y.is_physical_interface]}
 
259
 
 
260
    def __iter__(self):  # class iter
 
261
        for s in self._stanzas:
 
262
            yield s
 
263
 
 
264
 
 
265
def uniq_append(dst, src):
 
266
    for x in src:
 
267
        if x not in dst:
 
268
            dst.append(x)
 
269
    return dst
 
270
 
 
271
 
 
272
def IfaceStanza(name, family, method, options):
 
273
    """Convenience function to create a new "iface" stanza.
 
274
 
 
275
Maintains original options order but removes duplicates with the
 
276
exception of 'dns-*' options which are normlised as required by
 
277
resolvconf(8) and all the dns-* options are moved to the end.
 
278
 
 
279
    """
 
280
 
 
281
    dns_search = []
 
282
    dns_nameserver = []
 
283
    dns_sortlist = []
 
284
    unique_options = []
 
285
 
 
286
    for o in options:
 
287
        words = o.split()
 
288
        ident = words[0]
 
289
        if ident == "dns-nameservers":
 
290
            dns_nameserver = uniq_append(dns_nameserver, words[1:])
 
291
        elif ident == "dns-search":
 
292
            dns_search = uniq_append(dns_search, words[1:])
 
293
        elif ident == "dns-sortlist":
 
294
            dns_sortlist = uniq_append(dns_sortlist, words[1:])
 
295
        elif o not in unique_options:
 
296
            unique_options.append(o)
 
297
 
 
298
    if dns_nameserver:
 
299
        option = "dns-nameservers " + " ".join(dns_nameserver)
 
300
        unique_options.append(option)
 
301
 
 
302
    if dns_search:
 
303
        option = "dns-search " + " ".join(dns_search)
 
304
        unique_options.append(option)
 
305
 
 
306
    if dns_sortlist:
 
307
        option = "dns-sortlist " + " ".join(dns_sortlist)
 
308
        unique_options.append(option)
 
309
 
 
310
    return Stanza("iface {} {} {}".format(name, family, method), unique_options)
 
311
 
 
312
 
 
313
def AutoStanza(name):
 
314
    # Convenience function to create a new "auto" stanza.
 
315
    return Stanza("auto {}".format(name))
 
316
 
 
317
 
 
318
def print_stanza(s, stream=sys.stdout):
 
319
    print(s.definition, file=stream)
 
320
    for o in s.options:
 
321
        print("   ", o, file=stream)
 
322
 
 
323
 
 
324
def print_stanzas(stanzas, stream=sys.stdout):
 
325
    n = len(stanzas)
 
326
    for i, stanza in enumerate(stanzas):
 
327
        print_stanza(stanza, stream)
 
328
        if stanza.is_logical_interface and i + 1 < n:
 
329
            print(file=stream)
 
330
 
 
331
 
 
332
def shell_cmd(s):
 
333
    p = subprocess.Popen(s, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
334
    out, err = p.communicate()
 
335
    return [out, err, p.returncode]
 
336
 
 
337
 
 
338
def print_shell_cmd(s, verbose=True, exit_on_error=False):
 
339
    if verbose:
 
340
        print(s)
 
341
    out, err, retcode = shell_cmd(s)
 
342
    if out and len(out) > 0:
 
343
        print(out.decode().rstrip('\n'))
 
344
    if err and len(err) > 0:
 
345
        print(err.decode().rstrip('\n'))
 
346
    if exit_on_error and retcode != 0:
 
347
        exit(1)
 
348
 
 
349
 
 
350
def check_shell_cmd(s, verbose=False):
 
351
    if verbose:
 
352
        print(s)
 
353
    output = subprocess.check_output(s, shell=True, stderr=subprocess.STDOUT).strip().decode("utf-8")
 
354
    if verbose:
 
355
        print(output.rstrip('\n'))
 
356
    return output
 
357
 
 
358
 
 
359
def arg_parser():
 
360
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
 
361
    parser.add_argument('--bridge-prefix', help="bridge prefix", type=str, required=False, default='br-')
 
362
    parser.add_argument('--one-time-backup', help='A one time backup of filename', action='store_true', default=True, required=False)
 
363
    parser.add_argument('--activate', help='activate new configuration', action='store_true', default=False, required=False)
 
364
    parser.add_argument('--interface-to-bridge', help="interface to bridge", type=str, required=False)
 
365
    parser.add_argument('--bridge-name', help="bridge name", type=str, required=False)
 
366
    parser.add_argument('filename', help="interfaces(5) based filename")
 
367
    return parser
 
368
 
 
369
 
 
370
def main(args):
 
371
    if args.bridge_name and args.interface_to_bridge is None:
 
372
        sys.stderr.write("error: --interface-to-bridge required when using --bridge-name\n")
 
373
        exit(1)
 
374
 
 
375
    if args.interface_to_bridge and args.bridge_name is None:
 
376
        sys.stderr.write("error: --bridge-name required when using --interface-to-bridge\n")
 
377
        exit(1)
 
378
 
 
379
    stanzas = []
 
380
    config_parser = NetworkInterfaceParser(args.filename)
 
381
 
 
382
    # Bridging requires modifying 'auto' and 'iface' stanzas only.
 
383
    # Calling <iface>.bridge() will return a set of stanzas that cover
 
384
    # both of those stanzas. The 'elif' clause catches all the other
 
385
    # stanza types. The args.interface_to_bridge test is to bridge a
 
386
    # single interface only, which is only used for juju < 2.0. And if
 
387
    # that argument is specified then args.bridge_name takes
 
388
    # precedence over any args.bridge_prefix.
 
389
 
 
390
    for s in config_parser.stanzas():
 
391
        if s.is_logical_interface:
 
392
            if args.interface_to_bridge and args.interface_to_bridge != s.iface.name:
 
393
                if s.iface.has_auto_stanza:
 
394
                    stanzas.append(AutoStanza(s.iface.name))
 
395
                stanzas.append(s)
 
396
            else:
 
397
                stanzas.extend(s.iface.bridge(args.bridge_prefix, args.bridge_name))
 
398
        elif not s.is_physical_interface:
 
399
            stanzas.append(s)
 
400
 
 
401
    if not args.activate:
 
402
        print_stanzas(stanzas)
 
403
        exit(0)
 
404
 
 
405
    if args.one_time_backup:
 
406
        backup_file = "{}-before-add-juju-bridge".format(args.filename)
 
407
        if not os.path.isfile(backup_file):
 
408
            shutil.copy2(args.filename, backup_file)
 
409
 
 
410
    ifquery = "$(ifquery --interfaces={} --exclude=lo --list)".format(args.filename)
 
411
 
 
412
    print("**** Original configuration")
 
413
    print_shell_cmd("cat {}".format(args.filename))
 
414
    print_shell_cmd("ifconfig -a")
 
415
    print_shell_cmd("ifdown --exclude=lo --interfaces={} {}".format(args.filename, ifquery))
 
416
 
 
417
    print("**** Activating new configuration")
 
418
 
 
419
    with open(args.filename, 'w') as f:
 
420
        print_stanzas(stanzas, f)
 
421
        f.close()
 
422
 
 
423
    # On configurations that have bonds in 802.3ad mode there is a
 
424
    # race condition betweeen an immediate ifdown then ifup.
 
425
    #
 
426
    # On the h/w I have a 'sleep 0.1' is sufficient but to accommodate
 
427
    # other setups we arbitrarily choose something larger. We don't
 
428
    # want to massively slow bootstrap down but, equally, 0.1 may be
 
429
    # too small for other configurations.
 
430
 
 
431
    for s in stanzas:
 
432
        if s.is_logical_interface and s.iface.is_bonded:
 
433
            print("working around https://bugs.launchpad.net/ubuntu/+source/ifenslave/+bug/1269921")
 
434
            print("working around https://bugs.launchpad.net/juju-core/+bug/1594855")
 
435
            print_shell_cmd("sleep 3")
 
436
            break
 
437
 
 
438
    print_shell_cmd("cat {}".format(args.filename))
 
439
    print_shell_cmd("ifup --exclude=lo --interfaces={} {}".format(args.filename, ifquery))
 
440
    print_shell_cmd("ip link show up")
 
441
    print_shell_cmd("ifconfig -a")
 
442
    print_shell_cmd("ip route show")
 
443
    print_shell_cmd("brctl show")
 
444
 
 
445
# This script re-renders an interfaces(5) file to add a bridge to
 
446
# either all active interfaces, or a specific interface.
 
447
 
 
448
if __name__ == '__main__':
 
449
    main(arg_parser().parse_args())
 
450
`