github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/provider/maas/bridgescript.go (about)

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