3
# Copyright 2015 Canonical Ltd.
4
# Licensed under the AGPLv3, see LICENCE file for details.
7
# This file has been and should be formatted using pyfmt(1).
10
from __future__ import print_function
19
class SeekableIterator(object):
20
"""An iterator that supports relative seeking."""
22
def __init__(self, iterable):
23
self.iterable = iterable
29
def next(self): # Python 2
31
value = self.iterable[self.index]
37
def __next__(self): # Python 3
40
def seek(self, n, relative=False):
45
if self.index < 0 or self.index >= len(self.iterable):
49
class PhysicalInterface(object):
50
"""Represents a physical ('auto') interface."""
52
def __init__(self, definition):
53
self.name = definition.split()[1]
59
class LogicalInterface(object):
60
"""Represents a logical ('iface') interface."""
62
def __init__(self, definition, options=None):
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 ")]
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)
84
return self._bridge_alias(add_auto_stanza)
86
return self._bridge_vlan(prefix, bridge_name, add_auto_stanza)
88
return self._bridge_bond(prefix, bridge_name, add_auto_stanza)
90
return self._bridge_device(prefix, bridge_name)
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)
100
def _bridge_vlan(self, prefix, bridge_name, add_auto_stanza):
102
s1 = IfaceStanza(self.name, self.family, "manual", self.options)
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)
112
def _bridge_alias(self, add_auto_stanza):
115
stanzas.append(AutoStanza(self.name))
116
s1 = IfaceStanza(self.name, self.family, self.method, list(self.options))
120
def _bridge_bond(self, prefix, bridge_name, 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])
132
def _bridge_unchanged(self, add_auto_stanza):
135
stanzas.append(AutoStanza(self.name))
136
s1 = IfaceStanza(self.name, self.family, self.method, list(self.options))
141
class Stanza(object):
142
"""Represents one stanza together with all of its options."""
144
def __init__(self, definition, options=None):
147
self.definition = definition
148
self.options = options
149
self.is_logical_interface = definition.startswith('iface ')
150
self.is_physical_interface = definition.startswith('auto ')
153
if self.is_logical_interface:
154
self.iface = LogicalInterface(definition, self.options)
155
if self.is_physical_interface:
156
self.phy = PhysicalInterface(definition)
159
return self.definition
162
class NetworkInterfaceParser(object):
163
"""Parse a network interface file into a set of stanzas."""
166
def is_stanza(cls, s):
167
return re.match(r'^(iface|mapping|auto|allow-|source)', s)
169
def __init__(self, filename):
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)
179
def _parse_stanza(self, stanza_line, iterable):
181
for line in iterable:
183
if line.startswith('#') or line == "":
185
if self.is_stanza(line):
186
iterable.seek(-1, True)
188
stanza_options.append(line)
189
return Stanza(stanza_line.strip(), stanza_options)
192
return [x for x in self._stanzas]
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]}
197
def __iter__(self): # class iter
198
for s in self._stanzas:
202
def uniq_append(dst, src):
209
def IfaceStanza(name, family, method, options):
210
"""Convenience function to create a new "iface" stanza.
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.
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)
236
option = "dns-nameservers " + " ".join(dns_nameserver)
237
unique_options.append(option)
240
option = "dns-search " + " ".join(dns_search)
241
unique_options.append(option)
244
option = "dns-sortlist " + " ".join(dns_sortlist)
245
unique_options.append(option)
247
return Stanza("iface {} {} {}".format(name, family, method), unique_options)
250
def AutoStanza(name):
251
# Convenience function to create a new "auto" stanza.
252
return Stanza("auto {}".format(name))
255
def print_stanza(s, stream=sys.stdout):
256
print(s.definition, file=stream)
258
print(" ", o, file=stream)
261
def print_stanzas(stanzas, stream=sys.stdout):
263
for i, stanza in enumerate(stanzas):
264
print_stanza(stanza, stream)
265
if stanza.is_logical_interface and i + 1 < n:
270
p = subprocess.Popen(s, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
271
out, err = p.communicate()
272
return [out, err, p.returncode]
275
def print_shell_cmd(s, verbose=True, exit_on_error=False):
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:
287
def check_shell_cmd(s, verbose=False):
290
output = subprocess.check_output(s, shell=True, stderr=subprocess.STDOUT).strip().decode("utf-8")
292
print(output.rstrip('\n'))
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")
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")
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")
317
config_parser = NetworkInterfaceParser(args.filename)
318
physical_interfaces = config_parser.physical_interfaces()
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.
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:
333
stanzas.append(AutoStanza(s.iface.name))
336
stanzas.extend(s.iface.bridge(args.bridge_prefix, args.bridge_name, add_auto_stanza))
337
elif not s.is_physical_interface:
340
if not args.activate:
341
print_stanzas(stanzas)
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)
349
ifquery = "$(ifquery --interfaces={} --exclude=lo --list)".format(args.filename)
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))
356
print("**** Activating new configuration")
358
with open(args.filename, 'w') as f:
359
print_stanzas(stanzas, f)
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")
369
# This script re-renders an interfaces(5) file to add a bridge to
370
# either all active interfaces, or a specific interface.
372
if __name__ == '__main__':
373
main(arg_parser().parse_args())