~nskaggs/+junk/juju-packaging-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/provider/maas/add-juju-bridge.py

  • Committer: Nicholas Skaggs
  • Date: 2016-10-27 20:23:11 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161027202311-sux4jk2o73p1d6rg
Re-add src

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
# Copyright 2015 Canonical Ltd.
 
4
# Licensed under the AGPLv3, see LICENCE file for details.
 
5
 
 
6
#
 
7
# This file has been and should be formatted using pyfmt(1).
 
8
#
 
9
 
 
10
from __future__ import print_function
 
11
import argparse
 
12
import os
 
13
import re
 
14
import shutil
 
15
import subprocess
 
16
import sys
 
17
 
 
18
 
 
19
class SeekableIterator(object):
 
20
    """An iterator that supports relative seeking."""
 
21
 
 
22
    def __init__(self, iterable):
 
23
        self.iterable = iterable
 
24
        self.index = 0
 
25
 
 
26
    def __iter__(self):
 
27
        return self
 
28
 
 
29
    def next(self):  # Python 2
 
30
        try:
 
31
            value = self.iterable[self.index]
 
32
            self.index += 1
 
33
            return value
 
34
        except IndexError:
 
35
            raise StopIteration
 
36
 
 
37
    def __next__(self):  # Python 3
 
38
        return self.next()
 
39
 
 
40
    def seek(self, n, relative=False):
 
41
        if relative:
 
42
            self.index += n
 
43
        else:
 
44
            self.index = n
 
45
        if self.index < 0 or self.index >= len(self.iterable):
 
46
            raise IndexError
 
47
 
 
48
 
 
49
class PhysicalInterface(object):
 
50
    """Represents a physical ('auto') interface."""
 
51
 
 
52
    def __init__(self, definition):
 
53
        self.name = definition.split()[1]
 
54
 
 
55
    def __str__(self):
 
56
        return self.name
 
57
 
 
58
 
 
59
class LogicalInterface(object):
 
60
    """Represents a logical ('iface') interface."""
 
61
 
 
62
    def __init__(self, definition, options=None):
 
63
        if not options:
 
64
            options = []
 
65
        _, self.name, self.family, self.method = definition.split()
 
66
        self.options = options
 
67
        self.is_bonded = [x for x in self.options if "bond-" in x]
 
68
        self.is_alias = ":" in self.name
 
69
        self.is_vlan = [x for x in self.options if x.startswith("vlan-raw-device")]
 
70
        self.is_active = self.method == "dhcp" or self.method == "static"
 
71
        self.is_bridged = [x for x in self.options if x.startswith("bridge_ports ")]
 
72
 
 
73
    def __str__(self):
 
74
        return self.name
 
75
 
 
76
    # Returns an ordered set of stanzas to bridge this interface.
 
77
    def bridge(self, prefix, bridge_name, add_auto_stanza):
 
78
        if bridge_name is None:
 
79
            bridge_name = prefix + self.name
 
80
        # Note: the testing order here is significant.
 
81
        if not self.is_active or self.is_bridged:
 
82
            return self._bridge_unchanged(add_auto_stanza)
 
83
        elif self.is_alias:
 
84
            return self._bridge_alias(add_auto_stanza)
 
85
        elif self.is_vlan:
 
86
            return self._bridge_vlan(prefix, bridge_name, add_auto_stanza)
 
87
        elif self.is_bonded:
 
88
            return self._bridge_bond(prefix, bridge_name, add_auto_stanza)
 
89
        else:
 
90
            return self._bridge_device(prefix, bridge_name)
 
91
 
 
92
    def _bridge_device(self, prefix, bridge_name):
 
93
        s1 = IfaceStanza(self.name, self.family, "manual", [])
 
94
        s2 = AutoStanza(bridge_name)
 
95
        options = list(self.options)
 
96
        options.append("bridge_ports {}".format(self.name))
 
97
        s3 = IfaceStanza(bridge_name, self.family, self.method, options)
 
98
        return [s1, s2, s3]
 
99
 
 
100
    def _bridge_vlan(self, prefix, bridge_name, add_auto_stanza):
 
101
        stanzas = []
 
102
        s1 = IfaceStanza(self.name, self.family, "manual", self.options)
 
103
        stanzas.append(s1)
 
104
        if add_auto_stanza:
 
105
            stanzas.append(AutoStanza(bridge_name))
 
106
        options = [x for x in self.options if not x.startswith("vlan")]
 
107
        options.append("bridge_ports {}".format(self.name))
 
108
        s3 = IfaceStanza(bridge_name, self.family, self.method, options)
 
109
        stanzas.append(s3)
 
110
        return stanzas
 
111
 
 
112
    def _bridge_alias(self, add_auto_stanza):
 
113
        stanzas = []
 
114
        if add_auto_stanza:
 
115
            stanzas.append(AutoStanza(self.name))
 
116
        s1 = IfaceStanza(self.name, self.family, self.method, list(self.options))
 
117
        stanzas.append(s1)
 
118
        return stanzas
 
119
 
 
120
    def _bridge_bond(self, prefix, bridge_name, add_auto_stanza):
 
121
        stanzas = []
 
122
        if add_auto_stanza:
 
123
            stanzas.append(AutoStanza(self.name))
 
124
        s1 = IfaceStanza(self.name, self.family, "manual", list(self.options))
 
125
        s2 = AutoStanza(bridge_name)
 
126
        options = [x for x in self.options if not x.startswith("bond")]
 
127
        options.append("bridge_ports {}".format(self.name))
 
128
        s3 = IfaceStanza(bridge_name, self.family, self.method, options)
 
129
        stanzas.extend([s1, s2, s3])
 
130
        return stanzas
 
131
 
 
132
    def _bridge_unchanged(self, add_auto_stanza):
 
133
        stanzas = []
 
134
        if add_auto_stanza:
 
135
            stanzas.append(AutoStanza(self.name))
 
136
        s1 = IfaceStanza(self.name, self.family, self.method, list(self.options))
 
137
        stanzas.append(s1)
 
138
        return stanzas
 
139
 
 
140
 
 
141
class Stanza(object):
 
142
    """Represents one stanza together with all of its options."""
 
143
 
 
144
    def __init__(self, definition, options=None):
 
145
        if not options:
 
146
            options = []
 
147
        self.definition = definition
 
148
        self.options = options
 
149
        self.is_logical_interface = definition.startswith('iface ')
 
150
        self.is_physical_interface = definition.startswith('auto ')
 
151
        self.iface = None
 
152
        self.phy = None
 
153
        if self.is_logical_interface:
 
154
            self.iface = LogicalInterface(definition, self.options)
 
155
        if self.is_physical_interface:
 
156
            self.phy = PhysicalInterface(definition)
 
157
 
 
158
    def __str__(self):
 
159
        return self.definition
 
160
 
 
161
 
 
162
class NetworkInterfaceParser(object):
 
163
    """Parse a network interface file into a set of stanzas."""
 
164
 
 
165
    @classmethod
 
166
    def is_stanza(cls, s):
 
167
        return re.match(r'^(iface|mapping|auto|allow-|source)', s)
 
168
 
 
169
    def __init__(self, filename):
 
170
        self._stanzas = []
 
171
        with open(filename, 'r') as f:
 
172
            lines = f.readlines()
 
173
        line_iterator = SeekableIterator(lines)
 
174
        for line in line_iterator:
 
175
            if self.is_stanza(line):
 
176
                stanza = self._parse_stanza(line, line_iterator)
 
177
                self._stanzas.append(stanza)
 
178
 
 
179
    def _parse_stanza(self, stanza_line, iterable):
 
180
        stanza_options = []
 
181
        for line in iterable:
 
182
            line = line.strip()
 
183
            if line.startswith('#') or line == "":
 
184
                continue
 
185
            if self.is_stanza(line):
 
186
                iterable.seek(-1, True)
 
187
                break
 
188
            stanza_options.append(line)
 
189
        return Stanza(stanza_line.strip(), stanza_options)
 
190
 
 
191
    def stanzas(self):
 
192
        return [x for x in self._stanzas]
 
193
 
 
194
    def physical_interfaces(self):
 
195
        return {x.phy.name: x.phy for x in [y for y in self._stanzas if y.is_physical_interface]}
 
196
 
 
197
    def __iter__(self):  # class iter
 
198
        for s in self._stanzas:
 
199
            yield s
 
200
 
 
201
 
 
202
def uniq_append(dst, src):
 
203
    for x in src:
 
204
        if x not in dst:
 
205
            dst.append(x)
 
206
    return dst
 
207
 
 
208
 
 
209
def IfaceStanza(name, family, method, options):
 
210
    """Convenience function to create a new "iface" stanza.
 
211
 
 
212
Maintains original options order but removes duplicates with the
 
213
exception of 'dns-*' options which are normlised as required by
 
214
resolvconf(8) and all the dns-* options are moved to the end.
 
215
 
 
216
    """
 
217
 
 
218
    dns_search = []
 
219
    dns_nameserver = []
 
220
    dns_sortlist = []
 
221
    unique_options = []
 
222
 
 
223
    for o in options:
 
224
        words = o.split()
 
225
        ident = words[0]
 
226
        if ident == "dns-nameservers":
 
227
            dns_nameserver = uniq_append(dns_nameserver, words[1:])
 
228
        elif ident == "dns-search":
 
229
            dns_search = uniq_append(dns_search, words[1:])
 
230
        elif ident == "dns-sortlist":
 
231
            dns_sortlist = uniq_append(dns_sortlist, words[1:])
 
232
        elif o not in unique_options:
 
233
            unique_options.append(o)
 
234
 
 
235
    if dns_nameserver:
 
236
        option = "dns-nameservers " + " ".join(dns_nameserver)
 
237
        unique_options.append(option)
 
238
 
 
239
    if dns_search:
 
240
        option = "dns-search " + " ".join(dns_search)
 
241
        unique_options.append(option)
 
242
 
 
243
    if dns_sortlist:
 
244
        option = "dns-sortlist " + " ".join(dns_sortlist)
 
245
        unique_options.append(option)
 
246
 
 
247
    return Stanza("iface {} {} {}".format(name, family, method), unique_options)
 
248
 
 
249
 
 
250
def AutoStanza(name):
 
251
    # Convenience function to create a new "auto" stanza.
 
252
    return Stanza("auto {}".format(name))
 
253
 
 
254
 
 
255
def print_stanza(s, stream=sys.stdout):
 
256
    print(s.definition, file=stream)
 
257
    for o in s.options:
 
258
        print("   ", o, file=stream)
 
259
 
 
260
 
 
261
def print_stanzas(stanzas, stream=sys.stdout):
 
262
    n = len(stanzas)
 
263
    for i, stanza in enumerate(stanzas):
 
264
        print_stanza(stanza, stream)
 
265
        if stanza.is_logical_interface and i + 1 < n:
 
266
            print(file=stream)
 
267
 
 
268
 
 
269
def shell_cmd(s):
 
270
    p = subprocess.Popen(s, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
271
    out, err = p.communicate()
 
272
    return [out, err, p.returncode]
 
273
 
 
274
 
 
275
def print_shell_cmd(s, verbose=True, exit_on_error=False):
 
276
    if verbose:
 
277
        print(s)
 
278
    out, err, retcode = shell_cmd(s)
 
279
    if out and len(out) > 0:
 
280
        print(out.decode().rstrip('\n'))
 
281
    if err and len(err) > 0:
 
282
        print(err.decode().rstrip('\n'))
 
283
    if exit_on_error and retcode != 0:
 
284
        exit(1)
 
285
 
 
286
 
 
287
def check_shell_cmd(s, verbose=False):
 
288
    if verbose:
 
289
        print(s)
 
290
    output = subprocess.check_output(s, shell=True, stderr=subprocess.STDOUT).strip().decode("utf-8")
 
291
    if verbose:
 
292
        print(output.rstrip('\n'))
 
293
    return output
 
294
 
 
295
 
 
296
def arg_parser():
 
297
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
 
298
    parser.add_argument('--bridge-prefix', help="bridge prefix", type=str, required=False, default='br-')
 
299
    parser.add_argument('--one-time-backup', help='A one time backup of filename', action='store_true', default=True, required=False)
 
300
    parser.add_argument('--activate', help='activate new configuration', action='store_true', default=False, required=False)
 
301
    parser.add_argument('--interface-to-bridge', help="interface to bridge", type=str, required=False)
 
302
    parser.add_argument('--bridge-name', help="bridge name", type=str, required=False)
 
303
    parser.add_argument('filename', help="interfaces(5) based filename")
 
304
    return parser
 
305
 
 
306
 
 
307
def main(args):
 
308
    if args.bridge_name and args.interface_to_bridge is None:
 
309
        sys.stderr.write("error: --interface-to-bridge required when using --bridge-name\n")
 
310
        exit(1)
 
311
 
 
312
    if args.interface_to_bridge and args.bridge_name is None:
 
313
        sys.stderr.write("error: --bridge-name required when using --interface-to-bridge\n")
 
314
        exit(1)
 
315
 
 
316
    stanzas = []
 
317
    config_parser = NetworkInterfaceParser(args.filename)
 
318
    physical_interfaces = config_parser.physical_interfaces()
 
319
 
 
320
    # Bridging requires modifying 'auto' and 'iface' stanzas only.
 
321
    # Calling <iface>.bridge() will return a set of stanzas that cover
 
322
    # both of those stanzas. The 'elif' clause catches all the other
 
323
    # stanza types. The args.interface_to_bridge test is to bridge a
 
324
    # single interface only, which is only used for juju < 2.0. And if
 
325
    # that argument is specified then args.bridge_name takes
 
326
    # precendence over any args.bridge_prefix.
 
327
 
 
328
    for s in config_parser.stanzas():
 
329
        if s.is_logical_interface:
 
330
            add_auto_stanza = s.iface.name in physical_interfaces
 
331
            if args.interface_to_bridge and args.interface_to_bridge != s.iface.name:
 
332
                if add_auto_stanza:
 
333
                    stanzas.append(AutoStanza(s.iface.name))
 
334
                stanzas.append(s)
 
335
            else:
 
336
                stanzas.extend(s.iface.bridge(args.bridge_prefix, args.bridge_name, add_auto_stanza))
 
337
        elif not s.is_physical_interface:
 
338
            stanzas.append(s)
 
339
 
 
340
    if not args.activate:
 
341
        print_stanzas(stanzas)
 
342
        exit(0)
 
343
 
 
344
    if args.one_time_backup:
 
345
        backup_file = "{}-before-add-juju-bridge".format(args.filename)
 
346
        if not os.path.isfile(backup_file):
 
347
            shutil.copy2(args.filename, backup_file)
 
348
 
 
349
    ifquery = "$(ifquery --interfaces={} --exclude=lo --list)".format(args.filename)
 
350
 
 
351
    print("**** Original configuration")
 
352
    print_shell_cmd("cat {}".format(args.filename))
 
353
    print_shell_cmd("ifconfig -a")
 
354
    print_shell_cmd("ifdown --exclude=lo --interfaces={} {}".format(args.filename, ifquery))
 
355
 
 
356
    print("**** Activating new configuration")
 
357
 
 
358
    with open(args.filename, 'w') as f:
 
359
        print_stanzas(stanzas, f)
 
360
        f.close()
 
361
 
 
362
    print_shell_cmd("cat {}".format(args.filename))
 
363
    print_shell_cmd("ifup --exclude=lo --interfaces={} {}".format(args.filename, ifquery))
 
364
    print_shell_cmd("ip link show up")
 
365
    print_shell_cmd("ifconfig -a")
 
366
    print_shell_cmd("ip route show")
 
367
    print_shell_cmd("brctl show")
 
368
 
 
369
# This script re-renders an interfaces(5) file to add a bridge to
 
370
# either all active interfaces, or a specific interface.
 
371
 
 
372
if __name__ == '__main__':
 
373
    main(arg_parser().parse_args())