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