github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/provider/maas/add-juju-bridge.py (about)

     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      def bridge_now(self, prefix, bridge_name):
    77          # https://wiki.archlinux.org/index.php/Network_bridge
    78          # ip addr delete dev <interface name> <cidr>
    79          if bridge_name is None:
    80              bridge_name = prefix + self.name
    81  
    82          args = {
    83              'bridge': bridge_name,
    84              'parent': self.name,
    85          }
    86  
    87          for o in self.options:
    88              if o.startswith('vlan') or o.startswith('bond'):
    89                  continue
    90              option = o.split()
    91              if len(option) < 2:
    92                  args[option[0]] = ""
    93              else:
    94                  args[option[0]] = option[1]
    95  
    96          addr = check_shell_cmd('ip -d addr show {parent}'.format(**args))
    97          flags = re.search('<(.*?)>', addr).group(1).split(',')
    98          for exclude_flag in ['LOOPBACK', 'SLAVE']:
    99              if exclude_flag in flags:
   100                  # Don't bridge the loopback interface or slaves of bonds.
   101                  return
   102  
   103          # Save routes
   104          routes = check_shell_cmd('ip route show dev {parent}'.format(**args))
   105  
   106          print_shell_cmd('ip link add name {bridge} type bridge'.format(**args))
   107          print_shell_cmd('ip link set {bridge} up'.format(**args))
   108          print_shell_cmd('ip link set {parent} master {bridge}'.format(**args))
   109  
   110          if 'address' in args:
   111              print_shell_cmd('ip addr delete dev {parent} {address}'.format(**args))
   112  
   113              cmd = 'ip addr add dev {bridge} {address}'
   114              if 'netmask' in args:
   115                  cmd += '/{netmask}'
   116  
   117              print_shell_cmd(cmd.format(**args))
   118  
   119          for route in routes.splitlines():
   120              # ip route replace will add missing routes or update existing ones.
   121              print_shell_cmd('ip route replace {} dev {bridge}'.format(route, **args))
   122  
   123      # Returns an ordered set of stanzas to bridge this interface
   124      def bridge(self, prefix, bridge_name, add_auto_stanza):
   125          if bridge_name is None:
   126              bridge_name = prefix + self.name
   127  
   128          # Note: the testing order here is significant.
   129          if not self.is_active or self.is_bridged:
   130              return self._bridge_unchanged(add_auto_stanza)
   131          elif self.is_alias:
   132              return self._bridge_alias(add_auto_stanza)
   133          elif self.is_vlan:
   134              return self._bridge_vlan(bridge_name, add_auto_stanza)
   135          elif self.is_bonded:
   136              return self._bridge_bond(bridge_name, add_auto_stanza)
   137          else:
   138              return self._bridge_device(bridge_name)
   139  
   140      def _bridge_device(self, bridge_name):
   141          s1 = IfaceStanza(self.name, self.family, "manual", [])
   142          s2 = AutoStanza(bridge_name)
   143          options = list(self.options)
   144          options.append("bridge_ports {}".format(self.name))
   145          s3 = IfaceStanza(bridge_name, self.family, self.method, options)
   146          return [s1, s2, s3]
   147  
   148      def _bridge_vlan(self, bridge_name, add_auto_stanza):
   149          stanzas = []
   150          s1 = IfaceStanza(self.name, self.family, "manual", self.options)
   151          stanzas.append(s1)
   152          if add_auto_stanza:
   153              stanzas.append(AutoStanza(bridge_name))
   154          options = [x for x in self.options if not x.startswith("vlan")]
   155          options.append("bridge_ports {}".format(self.name))
   156          s3 = IfaceStanza(bridge_name, self.family, self.method, options)
   157          stanzas.append(s3)
   158          return stanzas
   159  
   160      def _bridge_alias(self, add_auto_stanza):
   161          stanzas = []
   162          if add_auto_stanza:
   163              stanzas.append(AutoStanza(self.name))
   164          s1 = IfaceStanza(self.name, self.family, self.method, list(self.options))
   165          stanzas.append(s1)
   166          return stanzas
   167  
   168      def _bridge_bond(self, bridge_name, add_auto_stanza):
   169          stanzas = []
   170          if add_auto_stanza:
   171              stanzas.append(AutoStanza(self.name))
   172          s1 = IfaceStanza(self.name, self.family, "manual", list(self.options))
   173          s2 = AutoStanza(bridge_name)
   174          options = [x for x in self.options if not x.startswith("bond")]
   175          options.append("bridge_ports {}".format(self.name))
   176          s3 = IfaceStanza(bridge_name, self.family, self.method, options)
   177          stanzas.extend([s1, s2, s3])
   178          return stanzas
   179  
   180      def _bridge_unchanged(self, add_auto_stanza):
   181          stanzas = []
   182          if add_auto_stanza:
   183              stanzas.append(AutoStanza(self.name))
   184          s1 = IfaceStanza(self.name, self.family, self.method, list(self.options))
   185          stanzas.append(s1)
   186          return stanzas
   187  
   188  
   189  class Stanza(object):
   190      """Represents one stanza together with all of its options."""
   191  
   192      def __init__(self, definition, options=None):
   193          if not options:
   194              options = []
   195          self.definition = definition
   196          self.options = options
   197          self.is_logical_interface = definition.startswith('iface ')
   198          self.is_physical_interface = definition.startswith('auto ')
   199          self.iface = None
   200          self.phy = None
   201          if self.is_logical_interface:
   202              self.iface = LogicalInterface(definition, self.options)
   203          if self.is_physical_interface:
   204              self.phy = PhysicalInterface(definition)
   205  
   206      def __str__(self):
   207          return self.definition
   208  
   209  
   210  class NetworkInterfaceParser(object):
   211      """Parse a network interface file into a set of stanzas."""
   212  
   213      @classmethod
   214      def is_stanza(cls, s):
   215          return re.match(r'^(iface|mapping|auto|allow-|source)', s)
   216  
   217      def __init__(self, filename):
   218          self._stanzas = []
   219          with open(filename, 'r') as f:
   220              lines = f.readlines()
   221          line_iterator = SeekableIterator(lines)
   222          for line in line_iterator:
   223              if self.is_stanza(line):
   224                  stanza = self._parse_stanza(line, line_iterator)
   225                  self._stanzas.append(stanza)
   226  
   227      def _parse_stanza(self, stanza_line, iterable):
   228          stanza_options = []
   229          for line in iterable:
   230              line = line.strip()
   231              if line.startswith('#') or line == "":
   232                  continue
   233              if self.is_stanza(line):
   234                  iterable.seek(-1, True)
   235                  break
   236              stanza_options.append(line)
   237          return Stanza(stanza_line.strip(), stanza_options)
   238  
   239      def stanzas(self):
   240          return [x for x in self._stanzas]
   241  
   242      def physical_interfaces(self):
   243          return {x.phy.name: x.phy for x in [y for y in self._stanzas if y.is_physical_interface]}
   244  
   245      def __iter__(self):  # class iter
   246          for s in self._stanzas:
   247              yield s
   248  
   249  
   250  def uniq_append(dst, src):
   251      for x in src:
   252          if x not in dst:
   253              dst.append(x)
   254      return dst
   255  
   256  
   257  def IfaceStanza(name, family, method, options):
   258      """Convenience function to create a new "iface" stanza.
   259  
   260  Maintains original options order but removes duplicates with the
   261  exception of 'dns-*' options which are normlised as required by
   262  resolvconf(8) and all the dns-* options are moved to the end.
   263  
   264      """
   265  
   266      dns_search = []
   267      dns_nameserver = []
   268      dns_sortlist = []
   269      unique_options = []
   270  
   271      for o in options:
   272          words = o.split()
   273          ident = words[0]
   274          if ident == "dns-nameservers":
   275              dns_nameserver = uniq_append(dns_nameserver, words[1:])
   276          elif ident == "dns-search":
   277              dns_search = uniq_append(dns_search, words[1:])
   278          elif ident == "dns-sortlist":
   279              dns_sortlist = uniq_append(dns_sortlist, words[1:])
   280          elif o not in unique_options:
   281              unique_options.append(o)
   282  
   283      if dns_nameserver:
   284          option = "dns-nameservers " + " ".join(dns_nameserver)
   285          unique_options.append(option)
   286  
   287      if dns_search:
   288          option = "dns-search " + " ".join(dns_search)
   289          unique_options.append(option)
   290  
   291      if dns_sortlist:
   292          option = "dns-sortlist " + " ".join(dns_sortlist)
   293          unique_options.append(option)
   294  
   295      return Stanza("iface {} {} {}".format(name, family, method), unique_options)
   296  
   297  
   298  def AutoStanza(name):
   299      # Convenience function to create a new "auto" stanza.
   300      return Stanza("auto {}".format(name))
   301  
   302  
   303  def print_stanza(s, stream=sys.stdout):
   304      print(s.definition, file=stream)
   305      for o in s.options:
   306          print("   ", o, file=stream)
   307  
   308  
   309  def print_stanzas(stanzas, stream=sys.stdout):
   310      n = len(stanzas)
   311      for i, stanza in enumerate(stanzas):
   312          print_stanza(stanza, stream)
   313          if stanza.is_logical_interface and i + 1 < n:
   314              print(file=stream)
   315  
   316  
   317  def shell_cmd(s):
   318      p = subprocess.Popen(s, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
   319      out, err = p.communicate()
   320      return [out, err, p.returncode]
   321  
   322  
   323  def print_shell_cmd(s, verbose=True, exit_on_error=False):
   324      if verbose:
   325          print(s)
   326      out, err, retcode = shell_cmd(s)
   327      if out and len(out) > 0:
   328          print(out.decode().rstrip('\n'))
   329      if err and len(err) > 0:
   330          print(err.decode().rstrip('\n'))
   331      if exit_on_error and retcode != 0:
   332          exit(1)
   333  
   334  
   335  def check_shell_cmd(s, verbose=False):
   336      if verbose:
   337          print(s)
   338      output = subprocess.check_output(s, shell=True, stderr=subprocess.STDOUT).strip().decode("utf-8")
   339      if verbose:
   340          print(output.rstrip('\n'))
   341      return output
   342  
   343  
   344  def arg_parser():
   345      parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
   346      parser.add_argument('--bridge-prefix', help="bridge prefix", type=str, required=False, default='br-')
   347      parser.add_argument('--one-time-backup', help='A one time backup of filename', action='store_true', default=True, required=False)
   348      parser.add_argument('--activate', help='activate new configuration', action='store_true', default=False, required=False)
   349      parser.add_argument('--interface-to-bridge', help="interface to bridge", type=str, required=False)
   350      parser.add_argument('--bridge-name', help="bridge name", type=str, required=False)
   351      parser.add_argument('filename', help="interfaces(5) based filename")
   352      return parser
   353  
   354  
   355  def main(args):
   356      if args.bridge_name and args.interface_to_bridge is None:
   357          sys.stderr.write("error: --interface-to-bridge required when using --bridge-name\n")
   358          exit(1)
   359  
   360      if args.interface_to_bridge and args.bridge_name is None:
   361          sys.stderr.write("error: --bridge-name required when using --interface-to-bridge\n")
   362          exit(1)
   363  
   364      stanzas = []
   365      config_parser = NetworkInterfaceParser(args.filename)
   366      physical_interfaces = config_parser.physical_interfaces()
   367  
   368      # Bridging requires modifying 'auto' and 'iface' stanzas only.
   369      # Calling <iface>.bridge() will return a set of stanzas that cover
   370      # both of those stanzas. The 'elif' clause catches all the other
   371      # stanza types. The args.interface_to_bridge test is to bridge a
   372      # single interface only, which is only used for juju < 2.0. And if
   373      # that argument is specified then args.bridge_name takes
   374      # precedence over any args.bridge_prefix.
   375  
   376      for s in config_parser.stanzas():
   377          if s.is_logical_interface:
   378              add_auto_stanza = s.iface.name in physical_interfaces
   379  
   380              if args.interface_to_bridge and args.interface_to_bridge != s.iface.name:
   381                  if add_auto_stanza:
   382                      stanzas.append(AutoStanza(s.iface.name))
   383                  stanzas.append(s)
   384              else:
   385                  stanza = s.iface.bridge(args.bridge_prefix, args.bridge_name, add_auto_stanza)
   386                  stanzas.extend(stanza)
   387  
   388          elif not s.is_physical_interface:
   389              stanzas.append(s)
   390  
   391      if not args.activate:
   392          print_stanzas(stanzas)
   393          exit(0)
   394  
   395      print("**** Original configuration")
   396      print_shell_cmd("cat {}".format(args.filename))
   397      print_shell_cmd("ip -d addr show")
   398      print_shell_cmd("ip route show")
   399  
   400      for s in config_parser.stanzas():
   401          if s.is_logical_interface:
   402              if not(args.interface_to_bridge and args.interface_to_bridge != s.iface.name):
   403                  s.iface.bridge_now(args.bridge_prefix, args.bridge_name)
   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      with open(args.filename, 'w') as f:
   411          print_stanzas(stanzas, f)
   412          f.close()
   413  
   414      print("**** New configuration")
   415      print_shell_cmd("cat {}".format(args.filename))
   416      print_shell_cmd("ip -d addr show")
   417      print_shell_cmd("ip route show")
   418  
   419  # This script re-renders an interfaces(5) file to add a bridge to
   420  # either all active interfaces, or a specific interface.
   421  
   422  if __name__ == '__main__':
   423      main(arg_parser().parse_args())