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