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