diff options
| -rwxr-xr-x | cloudinit/cmd/devel/net_convert.py | 14 | ||||
| -rwxr-xr-x | cloudinit/distros/__init__.py | 2 | ||||
| -rw-r--r-- | cloudinit/distros/opensuse.py | 15 | ||||
| -rw-r--r-- | cloudinit/distros/rhel.py | 10 | ||||
| -rw-r--r-- | cloudinit/net/eni.py | 2 | ||||
| -rw-r--r-- | cloudinit/net/netplan.py | 2 | ||||
| -rw-r--r-- | cloudinit/net/renderer.py | 9 | ||||
| -rw-r--r-- | cloudinit/net/sysconfig.py | 78 | ||||
| -rw-r--r-- | cloudinit/tests/helpers.py | 11 | ||||
| -rw-r--r-- | tests/unittests/test_datasource/test_configdrive.py | 6 | ||||
| -rw-r--r-- | tests/unittests/test_distros/test_netconfig.py | 971 | ||||
| -rw-r--r-- | tests/unittests/test_net.py | 544 | 
12 files changed, 1029 insertions, 635 deletions
| diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py index 271dc5ed..a0f58a0a 100755 --- a/cloudinit/cmd/devel/net_convert.py +++ b/cloudinit/cmd/devel/net_convert.py @@ -10,6 +10,7 @@ import yaml  from cloudinit.sources.helpers import openstack  from cloudinit.sources import DataSourceAzure as azure +from cloudinit import distros  from cloudinit.net import eni, netplan, network_state, sysconfig  from cloudinit import log @@ -36,6 +37,11 @@ def get_parser(parser=None):                          metavar="PATH",                          help="directory to place output in",                          required=True) +    parser.add_argument("-D", "--distro", +                        choices=[item for sublist in +                                 distros.OSFAMILIES.values() +                                 for item in sublist], +                        required=True)      parser.add_argument("-m", "--mac",                          metavar="name,mac",                          action='append', @@ -96,14 +102,20 @@ def handle_args(name, args):          sys.stderr.write('\n'.join([              "", "Internal State",              yaml.dump(ns, default_flow_style=False, indent=4), ""])) +    distro_cls = distros.fetch(args.distro) +    distro = distro_cls(args.distro, {}, None) +    config = {}      if args.output_kind == "eni":          r_cls = eni.Renderer +        config = distro.renderer_configs.get('eni')      elif args.output_kind == "netplan":          r_cls = netplan.Renderer +        config = distro.renderer_configs.get('netplan')      else:          r_cls = sysconfig.Renderer +        config = distro.renderer_configs.get('sysconfig') -    r = r_cls() +    r = r_cls(config=config)      sys.stderr.write(''.join([          "Read input format '%s' from '%s'.\n" % (              args.kind, args.network_data.name), diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index fde054e9..d9101ce6 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -91,7 +91,7 @@ class Distro(object):          LOG.debug("Selected renderer '%s' from priority list: %s",                    name, priority)          renderer = render_cls(config=self.renderer_configs.get(name)) -        renderer.render_network_config(network_config=network_config) +        renderer.render_network_config(network_config)          return []      def _find_tz_file(self, tz): diff --git a/cloudinit/distros/opensuse.py b/cloudinit/distros/opensuse.py index 9f90e95e..1fe896aa 100644 --- a/cloudinit/distros/opensuse.py +++ b/cloudinit/distros/opensuse.py @@ -28,13 +28,23 @@ class Distro(distros.Distro):      hostname_conf_fn = '/etc/HOSTNAME'      init_cmd = ['service']      locale_conf_fn = '/etc/sysconfig/language' -    network_conf_fn = '/etc/sysconfig/network' +    network_conf_fn = '/etc/sysconfig/network/config'      network_script_tpl = '/etc/sysconfig/network/ifcfg-%s'      resolve_conf_fn = '/etc/resolv.conf'      route_conf_tpl = '/etc/sysconfig/network/ifroute-%s'      systemd_hostname_conf_fn = '/etc/hostname'      systemd_locale_conf_fn = '/etc/locale.conf'      tz_local_fn = '/etc/localtime' +    renderer_configs = { +        'sysconfig': { +            'control': 'etc/sysconfig/network/config', +            'iface_templates': '%(base)s/network/ifcfg-%(name)s', +            'route_templates': { +                'ipv4': '%(base)s/network/ifroute-%(name)s', +                'ipv6': '%(base)s/network/ifroute-%(name)s', +            } +        } +    }      def __init__(self, name, cfg, paths):          distros.Distro.__init__(self, name, cfg, paths) @@ -208,6 +218,9 @@ class Distro(distros.Distro):                                              nameservers, searchservers)          return dev_names +    def _write_network_config(self, netconfig): +        return self._supported_write_network_config(netconfig) +      @property      def preferred_ntp_clients(self):          """The preferred ntp client is dependent on the version.""" diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py index 1fecb619..ff513438 100644 --- a/cloudinit/distros/rhel.py +++ b/cloudinit/distros/rhel.py @@ -39,6 +39,16 @@ class Distro(distros.Distro):      resolve_conf_fn = "/etc/resolv.conf"      tz_local_fn = "/etc/localtime"      usr_lib_exec = "/usr/libexec" +    renderer_configs = { +        'sysconfig': { +            'control': 'etc/sysconfig/network', +            'iface_templates': '%(base)s/network-scripts/ifcfg-%(name)s', +            'route_templates': { +                'ipv4': '%(base)s/network-scripts/route-%(name)s', +                'ipv6': '%(base)s/network-scripts/route6-%(name)s' +            } +        } +    }      def __init__(self, name, cfg, paths):          distros.Distro.__init__(self, name, cfg, paths) diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py index 80be2429..c6f631a9 100644 --- a/cloudinit/net/eni.py +++ b/cloudinit/net/eni.py @@ -480,7 +480,7 @@ class Renderer(renderer.Renderer):          return '\n\n'.join(['\n'.join(s) for s in sections]) + "\n" -    def render_network_state(self, network_state, target=None): +    def render_network_state(self, network_state, templates=None, target=None):          fpeni = util.target_path(target, self.eni_path)          util.ensure_dir(os.path.dirname(fpeni))          header = self.eni_header if self.eni_header else "" diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py index 6352e78c..bc1087f9 100644 --- a/cloudinit/net/netplan.py +++ b/cloudinit/net/netplan.py @@ -189,7 +189,7 @@ class Renderer(renderer.Renderer):          self._postcmds = config.get('postcmds', False)          self.clean_default = config.get('clean_default', True) -    def render_network_state(self, network_state, target): +    def render_network_state(self, network_state, templates=None, target=None):          # check network state for version          # if v2, then extract network_state.config          # else render_v2_from_state diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py index 57652e27..5f32e90f 100644 --- a/cloudinit/net/renderer.py +++ b/cloudinit/net/renderer.py @@ -45,11 +45,14 @@ class Renderer(object):          return content.getvalue()      @abc.abstractmethod -    def render_network_state(self, network_state, target=None): +    def render_network_state(self, network_state, templates=None, +                             target=None):          """Render network state.""" -    def render_network_config(self, network_config, target=None): +    def render_network_config(self, network_config, templates=None, +                              target=None):          return self.render_network_state( -            network_state=parse_net_config_data(network_config), target=target) +            network_state=parse_net_config_data(network_config), +            templates=templates, target=target)  # vi: ts=4 expandtab diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 3d719238..66e970e0 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -91,19 +91,20 @@ class ConfigMap(object):  class Route(ConfigMap):      """Represents a route configuration.""" -    route_fn_tpl_ipv4 = '%(base)s/network-scripts/route-%(name)s' -    route_fn_tpl_ipv6 = '%(base)s/network-scripts/route6-%(name)s' - -    def __init__(self, route_name, base_sysconf_dir): +    def __init__(self, route_name, base_sysconf_dir, +                 ipv4_tpl, ipv6_tpl):          super(Route, self).__init__()          self.last_idx = 1          self.has_set_default_ipv4 = False          self.has_set_default_ipv6 = False          self._route_name = route_name          self._base_sysconf_dir = base_sysconf_dir +        self.route_fn_tpl_ipv4 = ipv4_tpl +        self.route_fn_tpl_ipv6 = ipv6_tpl      def copy(self): -        r = Route(self._route_name, self._base_sysconf_dir) +        r = Route(self._route_name, self._base_sysconf_dir, +                  self.route_fn_tpl_ipv4, self.route_fn_tpl_ipv6)          r._conf = self._conf.copy()          r.last_idx = self.last_idx          r.has_set_default_ipv4 = self.has_set_default_ipv4 @@ -169,18 +170,22 @@ class Route(ConfigMap):  class NetInterface(ConfigMap):      """Represents a sysconfig/networking-script (and its config + children).""" -    iface_fn_tpl = '%(base)s/network-scripts/ifcfg-%(name)s' -      iface_types = {          'ethernet': 'Ethernet',          'bond': 'Bond',          'bridge': 'Bridge',      } -    def __init__(self, iface_name, base_sysconf_dir, kind='ethernet'): +    def __init__(self, iface_name, base_sysconf_dir, templates, +                 kind='ethernet'):          super(NetInterface, self).__init__()          self.children = [] -        self.routes = Route(iface_name, base_sysconf_dir) +        self.templates = templates +        route_tpl = self.templates.get('route_templates') +        self.routes = Route(iface_name, base_sysconf_dir, +                            ipv4_tpl=route_tpl.get('ipv4'), +                            ipv6_tpl=route_tpl.get('ipv6')) +        self.iface_fn_tpl = self.templates.get('iface_templates')          self.kind = kind          self._iface_name = iface_name @@ -213,7 +218,8 @@ class NetInterface(ConfigMap):                                       'name': self.name})      def copy(self, copy_children=False, copy_routes=False): -        c = NetInterface(self.name, self._base_sysconf_dir, kind=self._kind) +        c = NetInterface(self.name, self._base_sysconf_dir, +                         self.templates, kind=self._kind)          c._conf = self._conf.copy()          if copy_children:              c.children = list(self.children) @@ -251,6 +257,8 @@ class Renderer(renderer.Renderer):          ('bridge_bridgeprio', 'PRIO'),      ]) +    templates = {} +      def __init__(self, config=None):          if not config:              config = {} @@ -261,6 +269,11 @@ class Renderer(renderer.Renderer):          nm_conf_path = 'etc/NetworkManager/conf.d/99-cloud-init.conf'          self.networkmanager_conf_path = config.get('networkmanager_conf_path',                                                     nm_conf_path) +        self.templates = { +            'control': config.get('control'), +            'iface_templates': config.get('iface_templates'), +            'route_templates': config.get('route_templates'), +        }      @classmethod      def _render_iface_shared(cls, iface, iface_cfg): @@ -512,7 +525,7 @@ class Renderer(renderer.Renderer):          return content_str      @staticmethod -    def _render_networkmanager_conf(network_state): +    def _render_networkmanager_conf(network_state, templates=None):          content = networkmanager_conf.NetworkManagerConf("")          # If DNS server information is provided, configure @@ -556,14 +569,17 @@ class Renderer(renderer.Renderer):              cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)      @classmethod -    def _render_sysconfig(cls, base_sysconf_dir, network_state): +    def _render_sysconfig(cls, base_sysconf_dir, network_state, +                          templates=None):          '''Given state, return /etc/sysconfig files + contents''' +        if not templates: +            templates = cls.templates          iface_contents = {}          for iface in network_state.iter_interfaces():              if iface['type'] == "loopback":                  continue              iface_name = iface['name'] -            iface_cfg = NetInterface(iface_name, base_sysconf_dir) +            iface_cfg = NetInterface(iface_name, base_sysconf_dir, templates)              cls._render_iface_shared(iface, iface_cfg)              iface_contents[iface_name] = iface_cfg          cls._render_physical_interfaces(network_state, iface_contents) @@ -578,17 +594,21 @@ class Renderer(renderer.Renderer):                      if iface_cfg:                          contents[iface_cfg.path] = iface_cfg.to_string()              if iface_cfg.routes: -                contents[iface_cfg.routes.path_ipv4] = \ -                    iface_cfg.routes.to_string("ipv4") -                contents[iface_cfg.routes.path_ipv6] = \ -                    iface_cfg.routes.to_string("ipv6") +                for cpath, proto in zip([iface_cfg.routes.path_ipv4, +                                         iface_cfg.routes.path_ipv6], +                                        ["ipv4", "ipv6"]): +                    if cpath not in contents: +                        contents[cpath] = iface_cfg.routes.to_string(proto)          return contents -    def render_network_state(self, network_state, target=None): +    def render_network_state(self, network_state, templates=None, target=None): +        if not templates: +            templates = self.templates          file_mode = 0o644          base_sysconf_dir = util.target_path(target, self.sysconf_dir)          for path, data in self._render_sysconfig(base_sysconf_dir, -                                                 network_state).items(): +                                                 network_state, +                                                 templates=templates).items():              util.write_file(path, data, file_mode)          if self.dns_path:              dns_path = util.target_path(target, self.dns_path) @@ -598,7 +618,8 @@ class Renderer(renderer.Renderer):          if self.networkmanager_conf_path:              nm_conf_path = util.target_path(target,                                              self.networkmanager_conf_path) -            nm_conf_content = self._render_networkmanager_conf(network_state) +            nm_conf_content = self._render_networkmanager_conf(network_state, +                                                               templates)              if nm_conf_content:                  util.write_file(nm_conf_path, nm_conf_content, file_mode)          if self.netrules_path: @@ -606,13 +627,16 @@ class Renderer(renderer.Renderer):              netrules_path = util.target_path(target, self.netrules_path)              util.write_file(netrules_path, netrules_content, file_mode) -        # always write /etc/sysconfig/network configuration -        sysconfig_path = util.target_path(target, "etc/sysconfig/network") -        netcfg = [_make_header(), 'NETWORKING=yes'] -        if network_state.use_ipv6: -            netcfg.append('NETWORKING_IPV6=yes') -            netcfg.append('IPV6_AUTOCONF=no') -        util.write_file(sysconfig_path, "\n".join(netcfg) + "\n", file_mode) +        sysconfig_path = util.target_path(target, templates.get('control')) +        # Distros configuring /etc/sysconfig/network as a file e.g. Centos +        if sysconfig_path.endswith('network'): +            util.ensure_dir(os.path.dirname(sysconfig_path)) +            netcfg = [_make_header(), 'NETWORKING=yes'] +            if network_state.use_ipv6: +                netcfg.append('NETWORKING_IPV6=yes') +                netcfg.append('IPV6_AUTOCONF=no') +            util.write_file(sysconfig_path, +                            "\n".join(netcfg) + "\n", file_mode)  def available(target=None): diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py index de24e25d..9a21426e 100644 --- a/cloudinit/tests/helpers.py +++ b/cloudinit/tests/helpers.py @@ -16,9 +16,9 @@ import six  import unittest2  try: -    from contextlib import ExitStack +    from contextlib import ExitStack, contextmanager  except ImportError: -    from contextlib2 import ExitStack +    from contextlib2 import ExitStack, contextmanager  try:      from configparser import ConfigParser @@ -326,6 +326,13 @@ class FilesystemMockingTestCase(ResourceUsingTestCase):          self.patchOS(root)          return root +    @contextmanager +    def reRooted(self, root=None): +        try: +            yield self.reRoot(root) +        finally: +            self.patched_funcs.close() +  class HttprettyTestCase(CiTestCase):      # necessary as http_proxy gets in the way of httpretty diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py index 68400f22..7e6fcbbf 100644 --- a/tests/unittests/test_datasource/test_configdrive.py +++ b/tests/unittests/test_datasource/test_configdrive.py @@ -642,7 +642,7 @@ class TestConvertNetworkData(CiTestCase):              routes)          eni_renderer = eni.Renderer()          eni_renderer.render_network_state( -            network_state.parse_net_config_data(ncfg), self.tmp) +            network_state.parse_net_config_data(ncfg), target=self.tmp)          with open(os.path.join(self.tmp, "etc",                                 "network", "interfaces"), 'r') as f:              eni_rendering = f.read() @@ -664,7 +664,7 @@ class TestConvertNetworkData(CiTestCase):          eni_renderer = eni.Renderer()          eni_renderer.render_network_state( -            network_state.parse_net_config_data(ncfg), self.tmp) +            network_state.parse_net_config_data(ncfg), target=self.tmp)          with open(os.path.join(self.tmp, "etc",                                 "network", "interfaces"), 'r') as f:              eni_rendering = f.read() @@ -695,7 +695,7 @@ class TestConvertNetworkData(CiTestCase):                                            known_macs=KNOWN_MACS)          eni_renderer = eni.Renderer()          eni_renderer.render_network_state( -            network_state.parse_net_config_data(ncfg), self.tmp) +            network_state.parse_net_config_data(ncfg), target=self.tmp)          with open(os.path.join(self.tmp, "etc",                                 "network", "interfaces"), 'r') as f:              eni_rendering = f.read() diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py index 7765e408..740fb76c 100644 --- a/tests/unittests/test_distros/test_netconfig.py +++ b/tests/unittests/test_distros/test_netconfig.py @@ -2,24 +2,19 @@  import os  from six import StringIO -import stat  from textwrap import dedent  try:      from unittest import mock  except ImportError:      import mock -try: -    from contextlib import ExitStack -except ImportError: -    from contextlib2 import ExitStack  from cloudinit import distros  from cloudinit.distros.parsers.sys_conf import SysConf  from cloudinit import helpers -from cloudinit.net import eni  from cloudinit import settings -from cloudinit.tests.helpers import FilesystemMockingTestCase +from cloudinit.tests.helpers import ( +    FilesystemMockingTestCase, dir2dict, populate_dir)  from cloudinit import util @@ -82,7 +77,7 @@ V1_NET_CFG = {'config': [{'name': 'eth0',                            'type': 'physical'}],                'version': 1} -V1_NET_CFG_OUTPUT = """ +V1_NET_CFG_OUTPUT = """\  # This file is generated from information provided by  # the datasource.  Changes to it will not persist across an instance.  # To disable cloud-init's network configuration capabilities, write a file @@ -116,7 +111,7 @@ V1_NET_CFG_IPV6 = {'config': [{'name': 'eth0',                     'version': 1} -V1_TO_V2_NET_CFG_OUTPUT = """ +V1_TO_V2_NET_CFG_OUTPUT = """\  # This file is generated from information provided by  # the datasource.  Changes to it will not persist across an instance.  # To disable cloud-init's network configuration capabilities, write a file @@ -145,7 +140,7 @@ V2_NET_CFG = {  } -V2_TO_V2_NET_CFG_OUTPUT = """ +V2_TO_V2_NET_CFG_OUTPUT = """\  # This file is generated from information provided by  # the datasource.  Changes to it will not persist across an instance.  # To disable cloud-init's network configuration capabilities, write a file @@ -176,21 +171,10 @@ class WriteBuffer(object):          return self.buffer.getvalue() -class TestNetCfgDistro(FilesystemMockingTestCase): - -    frbsd_ifout = """\ -hn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500 -        options=51b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,TSO4,LRO> -        ether 00:15:5d:4c:73:00 -        inet6 fe80::215:5dff:fe4c:7300%hn0 prefixlen 64 scopeid 0x2 -        inet 10.156.76.127 netmask 0xfffffc00 broadcast 10.156.79.255 -        nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL> -        media: Ethernet autoselect (10Gbase-T <full-duplex>) -        status: active -""" +class TestNetCfgDistroBase(FilesystemMockingTestCase):      def setUp(self): -        super(TestNetCfgDistro, self).setUp() +        super(TestNetCfgDistroBase, self).setUp()          self.add_patch('cloudinit.util.system_is_snappy', 'm_snappy')          self.add_patch('cloudinit.util.system_info', 'm_sysinfo')          self.m_sysinfo.return_value = {'dist': ('Distro', '99.1', 'Codename')} @@ -204,144 +188,6 @@ hn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500          paths = helpers.Paths({})          return cls(dname, cfg.get('system_info'), paths) -    def test_simple_write_ub(self): -        ub_distro = self._get_distro('ubuntu') -        with ExitStack() as mocks: -            write_bufs = {} - -            def replace_write(filename, content, mode=0o644, omode="wb"): -                buf = WriteBuffer() -                buf.mode = mode -                buf.omode = omode -                buf.write(content) -                write_bufs[filename] = buf - -            mocks.enter_context( -                mock.patch.object(util, 'write_file', replace_write)) -            mocks.enter_context( -                mock.patch.object(os.path, 'isfile', return_value=False)) - -            ub_distro.apply_network(BASE_NET_CFG, False) - -            self.assertEqual(len(write_bufs), 1) -            eni_name = '/etc/network/interfaces.d/50-cloud-init.cfg' -            self.assertIn(eni_name, write_bufs) -            write_buf = write_bufs[eni_name] -            self.assertEqual(str(write_buf).strip(), BASE_NET_CFG.strip()) -            self.assertEqual(write_buf.mode, 0o644) - -    def test_apply_network_config_eni_ub(self): -        ub_distro = self._get_distro('ubuntu') -        with ExitStack() as mocks: -            write_bufs = {} - -            def replace_write(filename, content, mode=0o644, omode="wb"): -                buf = WriteBuffer() -                buf.mode = mode -                buf.omode = omode -                buf.write(content) -                write_bufs[filename] = buf - -            # eni availability checks -            mocks.enter_context( -                mock.patch.object(util, 'which', return_value=True)) -            mocks.enter_context( -                mock.patch.object(eni, 'available', return_value=True)) -            mocks.enter_context( -                mock.patch.object(util, 'ensure_dir')) -            mocks.enter_context( -                mock.patch.object(util, 'write_file', replace_write)) -            mocks.enter_context( -                mock.patch.object(os.path, 'isfile', return_value=False)) -            mocks.enter_context( -                mock.patch("cloudinit.net.eni.glob.glob", -                           return_value=[])) - -            ub_distro.apply_network_config(V1_NET_CFG, False) - -            self.assertEqual(len(write_bufs), 2) -            eni_name = '/etc/network/interfaces.d/50-cloud-init.cfg' -            self.assertIn(eni_name, write_bufs) -            write_buf = write_bufs[eni_name] -            self.assertEqual(str(write_buf).strip(), V1_NET_CFG_OUTPUT.strip()) -            self.assertEqual(write_buf.mode, 0o644) - -    def test_apply_network_config_v1_to_netplan_ub(self): -        renderers = ['netplan'] -        devlist = ['eth0', 'lo'] -        ub_distro = self._get_distro('ubuntu', renderers=renderers) -        with ExitStack() as mocks: -            write_bufs = {} - -            def replace_write(filename, content, mode=0o644, omode="wb"): -                buf = WriteBuffer() -                buf.mode = mode -                buf.omode = omode -                buf.write(content) -                write_bufs[filename] = buf - -            mocks.enter_context( -                mock.patch.object(util, 'which', return_value=True)) -            mocks.enter_context( -                mock.patch.object(util, 'write_file', replace_write)) -            mocks.enter_context( -                mock.patch.object(util, 'ensure_dir')) -            mocks.enter_context( -                mock.patch.object(util, 'subp', return_value=(0, 0))) -            mocks.enter_context( -                mock.patch.object(os.path, 'isfile', return_value=False)) -            mocks.enter_context( -                mock.patch("cloudinit.net.netplan.get_devicelist", -                           return_value=devlist)) - -            ub_distro.apply_network_config(V1_NET_CFG, False) - -            self.assertEqual(len(write_bufs), 1) -            netplan_name = '/etc/netplan/50-cloud-init.yaml' -            self.assertIn(netplan_name, write_bufs) -            write_buf = write_bufs[netplan_name] -            self.assertEqual(str(write_buf).strip(), -                             V1_TO_V2_NET_CFG_OUTPUT.strip()) -            self.assertEqual(write_buf.mode, 0o644) - -    def test_apply_network_config_v2_passthrough_ub(self): -        renderers = ['netplan'] -        devlist = ['eth0', 'lo'] -        ub_distro = self._get_distro('ubuntu', renderers=renderers) -        with ExitStack() as mocks: -            write_bufs = {} - -            def replace_write(filename, content, mode=0o644, omode="wb"): -                buf = WriteBuffer() -                buf.mode = mode -                buf.omode = omode -                buf.write(content) -                write_bufs[filename] = buf - -            mocks.enter_context( -                mock.patch.object(util, 'which', return_value=True)) -            mocks.enter_context( -                mock.patch.object(util, 'write_file', replace_write)) -            mocks.enter_context( -                mock.patch.object(util, 'ensure_dir')) -            mocks.enter_context( -                mock.patch.object(util, 'subp', return_value=(0, 0))) -            mocks.enter_context( -                mock.patch.object(os.path, 'isfile', return_value=False)) -            # FreeBSD does not have '/sys/class/net' file, -            # so we need mock here. -            mocks.enter_context( -                mock.patch.object(os, 'listdir', return_value=devlist)) -            ub_distro.apply_network_config(V2_NET_CFG, False) - -            self.assertEqual(len(write_bufs), 1) -            netplan_name = '/etc/netplan/50-cloud-init.yaml' -            self.assertIn(netplan_name, write_bufs) -            write_buf = write_bufs[netplan_name] -            self.assertEqual(str(write_buf).strip(), -                             V2_TO_V2_NET_CFG_OUTPUT.strip()) -            self.assertEqual(write_buf.mode, 0o644) -      def assertCfgEquals(self, blob1, blob2):          b1 = dict(SysConf(blob1.strip().splitlines()))          b2 = dict(SysConf(blob2.strip().splitlines())) @@ -353,6 +199,20 @@ hn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500          for (k, v) in b1.items():              self.assertEqual(v, b2[k]) + +class TestNetCfgDistroFreebsd(TestNetCfgDistroBase): + +    frbsd_ifout = """\ +hn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500 +        options=51b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,TSO4,LRO> +        ether 00:15:5d:4c:73:00 +        inet6 fe80::215:5dff:fe4c:7300%hn0 prefixlen 64 scopeid 0x2 +        inet 10.156.76.127 netmask 0xfffffc00 broadcast 10.156.79.255 +        nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL> +        media: Ethernet autoselect (10Gbase-T <full-duplex>) +        status: active +""" +      @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_list')      @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ifname_out')      def test_get_ip_nic_freebsd(self, ifname_out, iflist): @@ -376,349 +236,33 @@ hn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500          res = frbsd_distro.generate_fallback_config()          self.assertIsNotNone(res) -    def test_simple_write_rh(self): -        rh_distro = self._get_distro('rhel') - -        write_bufs = {} - -        def replace_write(filename, content, mode=0o644, omode="wb"): -            buf = WriteBuffer() -            buf.mode = mode -            buf.omode = omode -            buf.write(content) -            write_bufs[filename] = buf - -        with ExitStack() as mocks: -            mocks.enter_context( -                mock.patch.object(util, 'write_file', replace_write)) -            mocks.enter_context( -                mock.patch.object(util, 'load_file', return_value='')) -            mocks.enter_context( -                mock.patch.object(os.path, 'isfile', return_value=False)) - -            rh_distro.apply_network(BASE_NET_CFG, False) - -            self.assertEqual(len(write_bufs), 4) -            self.assertIn('/etc/sysconfig/network-scripts/ifcfg-lo', -                          write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-lo'] -            expected_buf = ''' -DEVICE="lo" -ONBOOT=yes -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) - -            self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth0', -                          write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth0'] -            expected_buf = ''' -DEVICE="eth0" -BOOTPROTO="static" -NETMASK="255.255.255.0" -IPADDR="192.168.1.5" -ONBOOT=yes -GATEWAY="192.168.1.254" -BROADCAST="192.168.1.0" -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) - -            self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth1', -                          write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth1'] -            expected_buf = ''' -DEVICE="eth1" -BOOTPROTO="dhcp" -ONBOOT=yes -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) - -            self.assertIn('/etc/sysconfig/network', write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network'] -            expected_buf = ''' -# Created by cloud-init v. 0.7 -NETWORKING=yes -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) - -    def test_apply_network_config_rh(self): -        renderers = ['sysconfig'] -        rh_distro = self._get_distro('rhel', renderers=renderers) - -        write_bufs = {} - -        def replace_write(filename, content, mode=0o644, omode="wb"): -            buf = WriteBuffer() -            buf.mode = mode -            buf.omode = omode -            buf.write(content) -            write_bufs[filename] = buf - -        with ExitStack() as mocks: -            # sysconfig availability checks -            mocks.enter_context( -                mock.patch.object(util, 'which', return_value=True)) -            mocks.enter_context( -                mock.patch.object(util, 'write_file', replace_write)) -            mocks.enter_context( -                mock.patch.object(util, 'load_file', return_value='')) -            mocks.enter_context( -                mock.patch.object(os.path, 'isfile', return_value=True)) - -            rh_distro.apply_network_config(V1_NET_CFG, False) - -            self.assertEqual(len(write_bufs), 5) - -            # eth0 -            self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth0', -                          write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth0'] -            expected_buf = ''' -# Created by cloud-init on instance boot automatically, do not edit. -# -BOOTPROTO=none -DEFROUTE=yes -DEVICE=eth0 -GATEWAY=192.168.1.254 -IPADDR=192.168.1.5 -NETMASK=255.255.255.0 -NM_CONTROLLED=no -ONBOOT=yes -TYPE=Ethernet -USERCTL=no -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) - -            # eth1 -            self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth1', -                          write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth1'] -            expected_buf = ''' -# Created by cloud-init on instance boot automatically, do not edit. -# -BOOTPROTO=dhcp -DEVICE=eth1 -NM_CONTROLLED=no -ONBOOT=yes -TYPE=Ethernet -USERCTL=no -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) - -            self.assertIn('/etc/sysconfig/network', write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network'] -            expected_buf = ''' -# Created by cloud-init v. 0.7 -NETWORKING=yes -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) - -    def test_write_ipv6_rhel(self): -        rh_distro = self._get_distro('rhel') - -        write_bufs = {} - -        def replace_write(filename, content, mode=0o644, omode="wb"): -            buf = WriteBuffer() -            buf.mode = mode -            buf.omode = omode -            buf.write(content) -            write_bufs[filename] = buf - -        with ExitStack() as mocks: -            mocks.enter_context( -                mock.patch.object(util, 'write_file', replace_write)) -            mocks.enter_context( -                mock.patch.object(util, 'load_file', return_value='')) -            mocks.enter_context( -                mock.patch.object(os.path, 'isfile', return_value=False)) -            rh_distro.apply_network(BASE_NET_CFG_IPV6, False) - -            self.assertEqual(len(write_bufs), 4) -            self.assertIn('/etc/sysconfig/network-scripts/ifcfg-lo', -                          write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-lo'] -            expected_buf = ''' -DEVICE="lo" -ONBOOT=yes -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) - -            self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth0', -                          write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth0'] -            expected_buf = ''' -DEVICE="eth0" -BOOTPROTO="static" -NETMASK="255.255.255.0" -IPADDR="192.168.1.5" -ONBOOT=yes -GATEWAY="192.168.1.254" -BROADCAST="192.168.1.0" -IPV6INIT=yes -IPV6ADDR="2607:f0d0:1002:0011::2" -IPV6_DEFAULTGW="2607:f0d0:1002:0011::1" -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) -            self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth1', -                          write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth1'] -            expected_buf = ''' -DEVICE="eth1" -BOOTPROTO="static" -NETMASK="255.255.255.0" -IPADDR="192.168.1.6" -ONBOOT=no -GATEWAY="192.168.1.254" -BROADCAST="192.168.1.0" -IPV6INIT=yes -IPV6ADDR="2607:f0d0:1002:0011::3" -IPV6_DEFAULTGW="2607:f0d0:1002:0011::1" -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) - -            self.assertIn('/etc/sysconfig/network', write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network'] -            expected_buf = ''' -# Created by cloud-init v. 0.7 -NETWORKING=yes -NETWORKING_IPV6=yes -IPV6_AUTOCONF=no -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) - -    def test_apply_network_config_ipv6_rh(self): -        renderers = ['sysconfig'] -        rh_distro = self._get_distro('rhel', renderers=renderers) - -        write_bufs = {} - -        def replace_write(filename, content, mode=0o644, omode="wb"): -            buf = WriteBuffer() -            buf.mode = mode -            buf.omode = omode -            buf.write(content) -            write_bufs[filename] = buf - -        with ExitStack() as mocks: -            mocks.enter_context( -                mock.patch.object(util, 'which', return_value=True)) -            mocks.enter_context( -                mock.patch.object(util, 'write_file', replace_write)) -            mocks.enter_context( -                mock.patch.object(util, 'load_file', return_value='')) -            mocks.enter_context( -                mock.patch.object(os.path, 'isfile', return_value=True)) - -            rh_distro.apply_network_config(V1_NET_CFG_IPV6, False) - -            self.assertEqual(len(write_bufs), 5) - -            self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth0', -                          write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth0'] -            expected_buf = ''' -# Created by cloud-init on instance boot automatically, do not edit. -# -BOOTPROTO=none -DEFROUTE=yes -DEVICE=eth0 -IPV6ADDR=2607:f0d0:1002:0011::2/64 -IPV6INIT=yes -IPV6_DEFAULTGW=2607:f0d0:1002:0011::1 -NM_CONTROLLED=no -ONBOOT=yes -TYPE=Ethernet -USERCTL=no -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) -            self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth1', -                          write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth1'] -            expected_buf = ''' -# Created by cloud-init on instance boot automatically, do not edit. -# -BOOTPROTO=dhcp -DEVICE=eth1 -NM_CONTROLLED=no -ONBOOT=yes -TYPE=Ethernet -USERCTL=no -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) - -            self.assertIn('/etc/sysconfig/network', write_bufs) -            write_buf = write_bufs['/etc/sysconfig/network'] -            expected_buf = ''' -# Created by cloud-init v. 0.7 -NETWORKING=yes -NETWORKING_IPV6=yes -IPV6_AUTOCONF=no -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) -      def test_simple_write_freebsd(self):          fbsd_distro = self._get_distro('freebsd') -        write_bufs = {} +        rc_conf = '/etc/rc.conf'          read_bufs = { -            '/etc/rc.conf': '', -            '/etc/resolv.conf': '', +            rc_conf: 'initial-rc-conf-not-validated', +            '/etc/resolv.conf': 'initial-resolv-conf-not-validated',          } -        def replace_write(filename, content, mode=0o644, omode="wb"): -            buf = WriteBuffer() -            buf.mode = mode -            buf.omode = omode -            buf.write(content) -            write_bufs[filename] = buf - -        def replace_read(fname, read_cb=None, quiet=False): -            if fname not in read_bufs: -                if fname in write_bufs: -                    return str(write_bufs[fname]) -                raise IOError("%s not found" % fname) -            else: -                if fname in write_bufs: -                    return str(write_bufs[fname]) -                return read_bufs[fname] - -        with ExitStack() as mocks: -            mocks.enter_context( -                mock.patch.object(util, 'subp', return_value=('vtnet0', ''))) -            mocks.enter_context( -                mock.patch.object(os.path, 'exists', return_value=False)) -            mocks.enter_context( -                mock.patch.object(util, 'write_file', replace_write)) -            mocks.enter_context( -                mock.patch.object(util, 'load_file', replace_read)) - -            fbsd_distro.apply_network(BASE_NET_CFG, False) - -            self.assertIn('/etc/rc.conf', write_bufs) -            write_buf = write_bufs['/etc/rc.conf'] -            expected_buf = ''' -ifconfig_vtnet0="192.168.1.5 netmask 255.255.255.0" -ifconfig_vtnet1="DHCP" -defaultrouter="192.168.1.254" -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) - -    def test_apply_network_config_fallback(self): +        tmpd = self.tmp_dir() +        populate_dir(tmpd, read_bufs) +        with self.reRooted(tmpd): +            with mock.patch("cloudinit.distros.freebsd.util.subp", +                            return_value=('vtnet0', '')): +                fbsd_distro.apply_network(BASE_NET_CFG, False) +                results = dir2dict(tmpd) + +        self.assertIn(rc_conf, results) +        self.assertCfgEquals( +            dedent('''\ +                ifconfig_vtnet0="192.168.1.5 netmask 255.255.255.0" +                ifconfig_vtnet1="DHCP" +                defaultrouter="192.168.1.254" +                '''), results[rc_conf]) +        self.assertEqual(0o644, get_mode(rc_conf, tmpd)) + +    def test_apply_network_config_fallback_freebsd(self):          fbsd_distro = self._get_distro('freebsd')          # a weak attempt to verify that we don't have an implementation @@ -735,68 +279,324 @@ defaultrouter="192.168.1.254"                          "subnets": [{"type": "dhcp"}]}],              'version': 1} -        write_bufs = {} +        rc_conf = '/etc/rc.conf'          read_bufs = { -            '/etc/rc.conf': '', -            '/etc/resolv.conf': '', +            rc_conf: 'initial-rc-conf-not-validated', +            '/etc/resolv.conf': 'initial-resolv-conf-not-validated',          } -        def replace_write(filename, content, mode=0o644, omode="wb"): -            buf = WriteBuffer() -            buf.mode = mode -            buf.omode = omode -            buf.write(content) -            write_bufs[filename] = buf - -        def replace_read(fname, read_cb=None, quiet=False): -            if fname not in read_bufs: -                if fname in write_bufs: -                    return str(write_bufs[fname]) -                raise IOError("%s not found" % fname) -            else: -                if fname in write_bufs: -                    return str(write_bufs[fname]) -                return read_bufs[fname] - -        with ExitStack() as mocks: -            mocks.enter_context( -                mock.patch.object(util, 'subp', return_value=('vtnet0', ''))) -            mocks.enter_context( -                mock.patch.object(os.path, 'exists', return_value=False)) -            mocks.enter_context( -                mock.patch.object(util, 'write_file', replace_write)) -            mocks.enter_context( -                mock.patch.object(util, 'load_file', replace_read)) - -            fbsd_distro.apply_network_config(mynetcfg, bring_up=False) - -            self.assertIn('/etc/rc.conf', write_bufs) -            write_buf = write_bufs['/etc/rc.conf'] -            expected_buf = ''' -ifconfig_vtnet0="DHCP" -''' -            self.assertCfgEquals(expected_buf, str(write_buf)) -            self.assertEqual(write_buf.mode, 0o644) +        tmpd = self.tmp_dir() +        populate_dir(tmpd, read_bufs) +        with self.reRooted(tmpd): +            with mock.patch("cloudinit.distros.freebsd.util.subp", +                            return_value=('vtnet0', '')): +                fbsd_distro.apply_network_config(mynetcfg, bring_up=False) +                results = dir2dict(tmpd) -    def test_simple_write_opensuse(self): -        """Opensuse network rendering writes appropriate sysconfg files.""" -        tmpdir = self.tmp_dir() -        self.patchOS(tmpdir) -        self.patchUtils(tmpdir) -        distro = self._get_distro('opensuse') +        self.assertIn(rc_conf, results) +        self.assertCfgEquals('ifconfig_vtnet0="DHCP"', results[rc_conf]) +        self.assertEqual(0o644, get_mode(rc_conf, tmpd)) -        distro.apply_network(BASE_NET_CFG, False) -        lo_path = os.path.join(tmpdir, 'etc/sysconfig/network/ifcfg-lo') -        eth0_path = os.path.join(tmpdir, 'etc/sysconfig/network/ifcfg-eth0') -        eth1_path = os.path.join(tmpdir, 'etc/sysconfig/network/ifcfg-eth1') +class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase): + +    def setUp(self): +        super(TestNetCfgDistroUbuntuEni, self).setUp() +        self.distro = self._get_distro('ubuntu', renderers=['eni']) + +    def eni_path(self): +        return '/etc/network/interfaces.d/50-cloud-init.cfg' + +    def _apply_and_verify_eni(self, apply_fn, config, expected_cfgs=None, +                              bringup=False): +        if not expected_cfgs: +            raise ValueError('expected_cfg must not be None') + +        tmpd = None +        with mock.patch('cloudinit.net.eni.available') as m_avail: +            m_avail.return_value = True +            with self.reRooted(tmpd) as tmpd: +                apply_fn(config, bringup) + +        results = dir2dict(tmpd) +        for cfgpath, expected in expected_cfgs.items(): +            print("----------") +            print(expected) +            print("^^^^ expected | rendered VVVVVVV") +            print(results[cfgpath]) +            print("----------") +            self.assertEqual(expected, results[cfgpath]) +            self.assertEqual(0o644, get_mode(cfgpath, tmpd)) + +    def test_simple_write_ub(self): +        expected_cfgs = { +            self.eni_path(): BASE_NET_CFG, +        } + +        # ub_distro.apply_network(BASE_NET_CFG, False) +        self._apply_and_verify_eni(self.distro.apply_network, +                                   BASE_NET_CFG, +                                   expected_cfgs=expected_cfgs.copy()) + +    def test_apply_network_config_eni_ub(self):          expected_cfgs = { -            lo_path: dedent(''' +            self.eni_path(): V1_NET_CFG_OUTPUT, +        } +        # ub_distro.apply_network_config(V1_NET_CFG, False) +        self._apply_and_verify_eni(self.distro.apply_network_config, +                                   V1_NET_CFG, +                                   expected_cfgs=expected_cfgs.copy()) + + +class TestNetCfgDistroUbuntuNetplan(TestNetCfgDistroBase): +    def setUp(self): +        super(TestNetCfgDistroUbuntuNetplan, self).setUp() +        self.distro = self._get_distro('ubuntu', renderers=['netplan']) +        self.devlist = ['eth0', 'lo'] + +    def _apply_and_verify_netplan(self, apply_fn, config, expected_cfgs=None, +                                  bringup=False): +        if not expected_cfgs: +            raise ValueError('expected_cfg must not be None') + +        tmpd = None +        with mock.patch('cloudinit.net.netplan.available', +                        return_value=True): +            with mock.patch("cloudinit.net.netplan.get_devicelist", +                            return_value=self.devlist): +                with self.reRooted(tmpd) as tmpd: +                    apply_fn(config, bringup) + +        results = dir2dict(tmpd) +        for cfgpath, expected in expected_cfgs.items(): +            print("----------") +            print(expected) +            print("^^^^ expected | rendered VVVVVVV") +            print(results[cfgpath]) +            print("----------") +            self.assertEqual(expected, results[cfgpath]) +            self.assertEqual(0o644, get_mode(cfgpath, tmpd)) + +    def netplan_path(self): +            return '/etc/netplan/50-cloud-init.yaml' + +    def test_apply_network_config_v1_to_netplan_ub(self): +        expected_cfgs = { +            self.netplan_path(): V1_TO_V2_NET_CFG_OUTPUT, +        } + +        # ub_distro.apply_network_config(V1_NET_CFG, False) +        self._apply_and_verify_netplan(self.distro.apply_network_config, +                                       V1_NET_CFG, +                                       expected_cfgs=expected_cfgs.copy()) + +    def test_apply_network_config_v2_passthrough_ub(self): +        expected_cfgs = { +            self.netplan_path(): V2_TO_V2_NET_CFG_OUTPUT, +        } +        # ub_distro.apply_network_config(V2_NET_CFG, False) +        self._apply_and_verify_netplan(self.distro.apply_network_config, +                                       V2_NET_CFG, +                                       expected_cfgs=expected_cfgs.copy()) + + +class TestNetCfgDistroRedhat(TestNetCfgDistroBase): + +    def setUp(self): +        super(TestNetCfgDistroRedhat, self).setUp() +        self.distro = self._get_distro('rhel', renderers=['sysconfig']) + +    def ifcfg_path(self, ifname): +        return '/etc/sysconfig/network-scripts/ifcfg-%s' % ifname + +    def control_path(self): +        return '/etc/sysconfig/network' + +    def _apply_and_verify(self, apply_fn, config, expected_cfgs=None, +                          bringup=False): +        if not expected_cfgs: +            raise ValueError('expected_cfg must not be None') + +        tmpd = None +        with mock.patch('cloudinit.net.sysconfig.available') as m_avail: +            m_avail.return_value = True +            with self.reRooted(tmpd) as tmpd: +                apply_fn(config, bringup) + +        results = dir2dict(tmpd) +        for cfgpath, expected in expected_cfgs.items(): +            self.assertCfgEquals(expected, results[cfgpath]) +            self.assertEqual(0o644, get_mode(cfgpath, tmpd)) + +    def test_simple_write_rh(self): +        expected_cfgs = { +            self.ifcfg_path('lo'): dedent("""\ +                DEVICE="lo" +                ONBOOT=yes +                """), +            self.ifcfg_path('eth0'): dedent("""\ +                DEVICE="eth0" +                BOOTPROTO="static" +                NETMASK="255.255.255.0" +                IPADDR="192.168.1.5" +                ONBOOT=yes +                GATEWAY="192.168.1.254" +                BROADCAST="192.168.1.0" +                """), +            self.ifcfg_path('eth1'): dedent("""\ +                DEVICE="eth1" +                BOOTPROTO="dhcp" +                ONBOOT=yes +                """), +            self.control_path(): dedent("""\ +                NETWORKING=yes +                """), +        } +        # rh_distro.apply_network(BASE_NET_CFG, False) +        self._apply_and_verify(self.distro.apply_network, +                               BASE_NET_CFG, +                               expected_cfgs=expected_cfgs.copy()) + +    def test_apply_network_config_rh(self): +        expected_cfgs = { +            self.ifcfg_path('eth0'): dedent("""\ +                BOOTPROTO=none +                DEFROUTE=yes +                DEVICE=eth0 +                GATEWAY=192.168.1.254 +                IPADDR=192.168.1.5 +                NETMASK=255.255.255.0 +                NM_CONTROLLED=no +                ONBOOT=yes +                TYPE=Ethernet +                USERCTL=no +                """), +            self.ifcfg_path('eth1'): dedent("""\ +                BOOTPROTO=dhcp +                DEVICE=eth1 +                NM_CONTROLLED=no +                ONBOOT=yes +                TYPE=Ethernet +                USERCTL=no +                """), +            self.control_path(): dedent("""\ +                NETWORKING=yes +                """), +        } +        # rh_distro.apply_network_config(V1_NET_CFG, False) +        self._apply_and_verify(self.distro.apply_network_config, +                               V1_NET_CFG, +                               expected_cfgs=expected_cfgs.copy()) + +    def test_write_ipv6_rhel(self): +        expected_cfgs = { +            self.ifcfg_path('lo'): dedent("""\ +                DEVICE="lo" +                ONBOOT=yes +                """), +            self.ifcfg_path('eth0'): dedent("""\ +                DEVICE="eth0" +                BOOTPROTO="static" +                NETMASK="255.255.255.0" +                IPADDR="192.168.1.5" +                ONBOOT=yes +                GATEWAY="192.168.1.254" +                BROADCAST="192.168.1.0" +                IPV6INIT=yes +                IPV6ADDR="2607:f0d0:1002:0011::2" +                IPV6_DEFAULTGW="2607:f0d0:1002:0011::1" +                """), +            self.ifcfg_path('eth1'): dedent("""\ +                DEVICE="eth1" +                BOOTPROTO="static" +                NETMASK="255.255.255.0" +                IPADDR="192.168.1.6" +                ONBOOT=no +                GATEWAY="192.168.1.254" +                BROADCAST="192.168.1.0" +                IPV6INIT=yes +                IPV6ADDR="2607:f0d0:1002:0011::3" +                IPV6_DEFAULTGW="2607:f0d0:1002:0011::1" +                """), +            self.control_path(): dedent("""\ +                NETWORKING=yes +                NETWORKING_IPV6=yes +                IPV6_AUTOCONF=no +                """), +        } +        # rh_distro.apply_network(BASE_NET_CFG_IPV6, False) +        self._apply_and_verify(self.distro.apply_network, +                               BASE_NET_CFG_IPV6, +                               expected_cfgs=expected_cfgs.copy()) + +    def test_apply_network_config_ipv6_rh(self): +        expected_cfgs = { +            self.ifcfg_path('eth0'): dedent("""\ +                BOOTPROTO=none +                DEFROUTE=yes +                DEVICE=eth0 +                IPV6ADDR=2607:f0d0:1002:0011::2/64 +                IPV6INIT=yes +                IPV6_DEFAULTGW=2607:f0d0:1002:0011::1 +                NM_CONTROLLED=no +                ONBOOT=yes +                TYPE=Ethernet +                USERCTL=no +                """), +            self.ifcfg_path('eth1'): dedent("""\ +                BOOTPROTO=dhcp +                DEVICE=eth1 +                NM_CONTROLLED=no +                ONBOOT=yes +                TYPE=Ethernet +                USERCTL=no +                """), +            self.control_path(): dedent("""\ +                NETWORKING=yes +                NETWORKING_IPV6=yes +                IPV6_AUTOCONF=no +                """), +            } +        # rh_distro.apply_network_config(V1_NET_CFG_IPV6, False) +        self._apply_and_verify(self.distro.apply_network_config, +                               V1_NET_CFG_IPV6, +                               expected_cfgs=expected_cfgs.copy()) + + +class TestNetCfgDistroOpensuse(TestNetCfgDistroBase): + +    def setUp(self): +        super(TestNetCfgDistroOpensuse, self).setUp() +        self.distro = self._get_distro('opensuse', renderers=['sysconfig']) + +    def ifcfg_path(self, ifname): +        return '/etc/sysconfig/network/ifcfg-%s' % ifname + +    def _apply_and_verify(self, apply_fn, config, expected_cfgs=None, +                          bringup=False): +        if not expected_cfgs: +            raise ValueError('expected_cfg must not be None') + +        tmpd = None +        with mock.patch('cloudinit.net.sysconfig.available') as m_avail: +            m_avail.return_value = True +            with self.reRooted(tmpd) as tmpd: +                apply_fn(config, bringup) + +        results = dir2dict(tmpd) +        for cfgpath, expected in expected_cfgs.items(): +            self.assertCfgEquals(expected, results[cfgpath]) +            self.assertEqual(0o644, get_mode(cfgpath, tmpd)) + +    def test_simple_write_opensuse(self): +        """Opensuse network rendering writes appropriate sysconfig files.""" +        expected_cfgs = { +            self.ifcfg_path('lo'): dedent('''                  STARTMODE="auto"                  USERCONTROL="no"                  FIREWALL="no"                  '''), -            eth0_path: dedent(''' +            self.ifcfg_path('eth0'): dedent('''                  BOOTPROTO="static"                  BROADCAST="192.168.1.0"                  GATEWAY="192.168.1.254" @@ -806,18 +606,77 @@ ifconfig_vtnet0="DHCP"                  USERCONTROL="no"                  ETHTOOL_OPTIONS=""                  '''), -            eth1_path: dedent(''' +            self.ifcfg_path('eth1'): dedent('''                  BOOTPROTO="dhcp"                  STARTMODE="auto"                  USERCONTROL="no"                  ETHTOOL_OPTIONS=""                  ''')          } -        for cfgpath in (lo_path, eth0_path, eth1_path): -            self.assertCfgEquals( -                expected_cfgs[cfgpath], -                util.load_file(cfgpath)) -            file_stat = os.stat(cfgpath) -            self.assertEqual(0o644, stat.S_IMODE(file_stat.st_mode)) + +        # distro.apply_network(BASE_NET_CFG, False) +        self._apply_and_verify(self.distro.apply_network, +                               BASE_NET_CFG, +                               expected_cfgs=expected_cfgs.copy()) + +    def test_apply_network_config_opensuse(self): +        """Opensuse uses apply_network_config and renders sysconfig""" +        expected_cfgs = { +            self.ifcfg_path('eth0'): dedent("""\ +                BOOTPROTO=none +                DEFROUTE=yes +                DEVICE=eth0 +                GATEWAY=192.168.1.254 +                IPADDR=192.168.1.5 +                NETMASK=255.255.255.0 +                NM_CONTROLLED=no +                ONBOOT=yes +                TYPE=Ethernet +                USERCTL=no +                """), +            self.ifcfg_path('eth1'): dedent("""\ +                BOOTPROTO=dhcp +                DEVICE=eth1 +                NM_CONTROLLED=no +                ONBOOT=yes +                TYPE=Ethernet +                USERCTL=no +                """), +        } +        self._apply_and_verify(self.distro.apply_network_config, +                               V1_NET_CFG, +                               expected_cfgs=expected_cfgs.copy()) + +    def test_apply_network_config_ipv6_opensuse(self): +        """Opensuse uses apply_network_config and renders sysconfig w/ipv6""" +        expected_cfgs = { +            self.ifcfg_path('eth0'): dedent("""\ +                BOOTPROTO=none +                DEFROUTE=yes +                DEVICE=eth0 +                IPV6ADDR=2607:f0d0:1002:0011::2/64 +                IPV6INIT=yes +                IPV6_DEFAULTGW=2607:f0d0:1002:0011::1 +                NM_CONTROLLED=no +                ONBOOT=yes +                TYPE=Ethernet +                USERCTL=no +            """), +            self.ifcfg_path('eth1'): dedent("""\ +                BOOTPROTO=dhcp +                DEVICE=eth1 +                NM_CONTROLLED=no +                ONBOOT=yes +                TYPE=Ethernet +                USERCTL=no +            """), +        } +        self._apply_and_verify(self.distro.apply_network_config, +                               V1_NET_CFG_IPV6, +                               expected_cfgs=expected_cfgs.copy()) + + +def get_mode(path, target=None): +    return os.stat(util.target_path(target, path)).st_mode & 0o777  # vi: ts=4 expandtab diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 58e5ea14..05d5c72c 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -1,6 +1,7 @@  # This file is part of cloud-init. See LICENSE file for license information.  from cloudinit import net +from cloudinit import distros  from cloudinit.net import cmdline  from cloudinit.net import (      eni, interface_has_own_mac, natural_sort_key, netplan, network_state, @@ -129,7 +130,40 @@ OS_SAMPLES = [          'in_macs': {              'fa:16:3e:ed:9a:59': 'eth0',          }, -        'out_sysconfig': [ +        'out_sysconfig_opensuse': [ +            ('etc/sysconfig/network/ifcfg-eth0', +             """ +# Created by cloud-init on instance boot automatically, do not edit. +# +BOOTPROTO=none +DEFROUTE=yes +DEVICE=eth0 +GATEWAY=172.19.3.254 +HWADDR=fa:16:3e:ed:9a:59 +IPADDR=172.19.1.34 +NETMASK=255.255.252.0 +NM_CONTROLLED=no +ONBOOT=yes +TYPE=Ethernet +USERCTL=no +""".lstrip()), +            ('etc/resolv.conf', +             """ +; Created by cloud-init on instance boot automatically, do not edit. +; +nameserver 172.19.0.12 +""".lstrip()), +            ('etc/NetworkManager/conf.d/99-cloud-init.conf', +             """ +# Created by cloud-init on instance boot automatically, do not edit. +# +[main] +dns = none +""".lstrip()), +            ('etc/udev/rules.d/70-persistent-net.rules', +             "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ', +                      'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))], +        'out_sysconfig_rhel': [              ('etc/sysconfig/network-scripts/ifcfg-eth0',               """  # Created by cloud-init on instance boot automatically, do not edit. @@ -162,6 +196,7 @@ dns = none              ('etc/udev/rules.d/70-persistent-net.rules',               "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',                        'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))] +      },      {          'in_data': { @@ -195,7 +230,42 @@ dns = none          'in_macs': {              'fa:16:3e:ed:9a:59': 'eth0',          }, -        'out_sysconfig': [ +        'out_sysconfig_opensuse': [ +            ('etc/sysconfig/network/ifcfg-eth0', +             """ +# Created by cloud-init on instance boot automatically, do not edit. +# +BOOTPROTO=none +DEFROUTE=yes +DEVICE=eth0 +GATEWAY=172.19.3.254 +HWADDR=fa:16:3e:ed:9a:59 +IPADDR=172.19.1.34 +IPADDR1=10.0.0.10 +NETMASK=255.255.252.0 +NETMASK1=255.255.255.0 +NM_CONTROLLED=no +ONBOOT=yes +TYPE=Ethernet +USERCTL=no +""".lstrip()), +            ('etc/resolv.conf', +             """ +; Created by cloud-init on instance boot automatically, do not edit. +; +nameserver 172.19.0.12 +""".lstrip()), +            ('etc/NetworkManager/conf.d/99-cloud-init.conf', +             """ +# Created by cloud-init on instance boot automatically, do not edit. +# +[main] +dns = none +""".lstrip()), +            ('etc/udev/rules.d/70-persistent-net.rules', +             "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ', +                      'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))], +        'out_sysconfig_rhel': [              ('etc/sysconfig/network-scripts/ifcfg-eth0',               """  # Created by cloud-init on instance boot automatically, do not edit. @@ -230,6 +300,7 @@ dns = none              ('etc/udev/rules.d/70-persistent-net.rules',               "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',                        'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))] +      },      {          'in_data': { @@ -283,7 +354,44 @@ dns = none          'in_macs': {              'fa:16:3e:ed:9a:59': 'eth0',          }, -        'out_sysconfig': [ +        'out_sysconfig_opensuse': [ +            ('etc/sysconfig/network/ifcfg-eth0', +             """ +# Created by cloud-init on instance boot automatically, do not edit. +# +BOOTPROTO=none +DEFROUTE=yes +DEVICE=eth0 +GATEWAY=172.19.3.254 +HWADDR=fa:16:3e:ed:9a:59 +IPADDR=172.19.1.34 +IPV6ADDR=2001:DB8::10/64 +IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64" +IPV6INIT=yes +IPV6_DEFAULTGW=2001:DB8::1 +NETMASK=255.255.252.0 +NM_CONTROLLED=no +ONBOOT=yes +TYPE=Ethernet +USERCTL=no +""".lstrip()), +            ('etc/resolv.conf', +             """ +; Created by cloud-init on instance boot automatically, do not edit. +; +nameserver 172.19.0.12 +""".lstrip()), +            ('etc/NetworkManager/conf.d/99-cloud-init.conf', +             """ +# Created by cloud-init on instance boot automatically, do not edit. +# +[main] +dns = none +""".lstrip()), +            ('etc/udev/rules.d/70-persistent-net.rules', +             "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ', +                      'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))], +        'out_sysconfig_rhel': [              ('etc/sysconfig/network-scripts/ifcfg-eth0',               """  # Created by cloud-init on instance boot automatically, do not edit. @@ -1154,7 +1262,59 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true               version: 2          """), -        'expected_sysconfig': { +        'expected_sysconfig_opensuse': { +            'ifcfg-bond0': textwrap.dedent("""\ +        BONDING_MASTER=yes +        BONDING_OPTS="mode=active-backup xmit_hash_policy=layer3+4 miimon=100" +        BONDING_SLAVE0=bond0s0 +        BONDING_SLAVE1=bond0s1 +        BOOTPROTO=none +        DEFROUTE=yes +        DEVICE=bond0 +        GATEWAY=192.168.0.1 +        MACADDR=aa:bb:cc:dd:e8:ff +        IPADDR=192.168.0.2 +        IPADDR1=192.168.1.2 +        IPV6ADDR=2001:1::1/92 +        IPV6INIT=yes +        MTU=9000 +        NETMASK=255.255.255.0 +        NETMASK1=255.255.255.0 +        NM_CONTROLLED=no +        ONBOOT=yes +        TYPE=Bond +        USERCTL=no +        """), +            'ifcfg-bond0s0': textwrap.dedent("""\ +        BOOTPROTO=none +        DEVICE=bond0s0 +        HWADDR=aa:bb:cc:dd:e8:00 +        MASTER=bond0 +        NM_CONTROLLED=no +        ONBOOT=yes +        SLAVE=yes +        TYPE=Ethernet +        USERCTL=no +        """), +            'ifroute-bond0': textwrap.dedent("""\ +        ADDRESS0=10.1.3.0 +        GATEWAY0=192.168.0.3 +        NETMASK0=255.255.255.0 +        """), +            'ifcfg-bond0s1': textwrap.dedent("""\ +        BOOTPROTO=none +        DEVICE=bond0s1 +        HWADDR=aa:bb:cc:dd:e8:01 +        MASTER=bond0 +        NM_CONTROLLED=no +        ONBOOT=yes +        SLAVE=yes +        TYPE=Ethernet +        USERCTL=no +        """), +        }, + +        'expected_sysconfig_rhel': {              'ifcfg-bond0': textwrap.dedent("""\          BONDING_MASTER=yes          BONDING_OPTS="mode=active-backup xmit_hash_policy=layer3+4 miimon=100" @@ -1527,7 +1687,7 @@ class TestGenerateFallbackConfig(CiTestCase):          # don't set rulepath so eni writes them          renderer = eni.Renderer(              {'eni_path': 'interfaces', 'netrules_path': 'netrules'}) -        renderer.render_network_state(ns, render_dir) +        renderer.render_network_state(ns, target=render_dir)          self.assertTrue(os.path.exists(os.path.join(render_dir,                                                      'interfaces'))) @@ -1591,7 +1751,7 @@ iface eth0 inet dhcp          # don't set rulepath so eni writes them          renderer = eni.Renderer(              {'eni_path': 'interfaces', 'netrules_path': 'netrules'}) -        renderer.render_network_state(ns, render_dir) +        renderer.render_network_state(ns, target=render_dir)          self.assertTrue(os.path.exists(os.path.join(render_dir,                                                      'interfaces'))) @@ -1682,7 +1842,7 @@ iface eth1 inet dhcp          self.assertEqual(0, mock_settle.call_count) -class TestSysConfigRendering(CiTestCase): +class TestRhelSysConfigRendering(CiTestCase):      with_logs = True @@ -1690,6 +1850,13 @@ class TestSysConfigRendering(CiTestCase):      header = ('# Created by cloud-init on instance boot automatically, '                'do not edit.\n#\n') +    expected_name = 'expected_sysconfig' + +    def _get_renderer(self): +        distro_cls = distros.fetch('rhel') +        return sysconfig.Renderer( +            config=distro_cls.renderer_configs.get('sysconfig')) +      def _render_and_read(self, network_config=None, state=None, dir=None):          if dir is None:              dir = self.tmp_dir() @@ -1701,8 +1868,8 @@ class TestSysConfigRendering(CiTestCase):          else:              raise ValueError("Expected data or state, got neither") -        renderer = sysconfig.Renderer() -        renderer.render_network_state(ns, dir) +        renderer = self._get_renderer() +        renderer.render_network_state(ns, target=dir)          return dir2dict(dir)      def _compare_files_to_expected(self, expected, found): @@ -1745,8 +1912,8 @@ class TestSysConfigRendering(CiTestCase):          render_dir = os.path.join(tmp_dir, "render")          os.makedirs(render_dir) -        renderer = sysconfig.Renderer() -        renderer.render_network_state(ns, render_dir) +        renderer = self._get_renderer() +        renderer.render_network_state(ns, target=render_dir)          render_file = 'etc/sysconfig/network-scripts/ifcfg-eth1000'          with open(os.path.join(render_dir, render_file)) as fh: @@ -1797,9 +1964,9 @@ USERCTL=no          network_cfg = openstack.convert_net_json(net_json, known_macs=macs)          ns = network_state.parse_net_config_data(network_cfg,                                                   skip_broken=False) -        renderer = sysconfig.Renderer() +        renderer = self._get_renderer()          with self.assertRaises(ValueError): -            renderer.render_network_state(ns, render_dir) +            renderer.render_network_state(ns, target=render_dir)          self.assertEqual([], os.listdir(render_dir))      def test_multiple_ipv6_default_gateways(self): @@ -1835,9 +2002,9 @@ USERCTL=no          network_cfg = openstack.convert_net_json(net_json, known_macs=macs)          ns = network_state.parse_net_config_data(network_cfg,                                                   skip_broken=False) -        renderer = sysconfig.Renderer() +        renderer = self._get_renderer()          with self.assertRaises(ValueError): -            renderer.render_network_state(ns, render_dir) +            renderer.render_network_state(ns, target=render_dir)          self.assertEqual([], os.listdir(render_dir))      def test_openstack_rendering_samples(self): @@ -1849,12 +2016,13 @@ USERCTL=no                  ex_input, known_macs=ex_mac_addrs)              ns = network_state.parse_net_config_data(network_cfg,                                                       skip_broken=False) -            renderer = sysconfig.Renderer() +            renderer = self._get_renderer()              # render a multiple times to simulate reboots -            renderer.render_network_state(ns, render_dir) -            renderer.render_network_state(ns, render_dir) -            renderer.render_network_state(ns, render_dir) -            for fn, expected_content in os_sample.get('out_sysconfig', []): +            renderer.render_network_state(ns, target=render_dir) +            renderer.render_network_state(ns, target=render_dir) +            renderer.render_network_state(ns, target=render_dir) +            for fn, expected_content in os_sample.get('out_sysconfig_rhel', +                                                      []):                  with open(os.path.join(render_dir, fn)) as fh:                      self.assertEqual(expected_content, fh.read()) @@ -1862,8 +2030,8 @@ USERCTL=no          ns = network_state.parse_net_config_data(CONFIG_V1_SIMPLE_SUBNET)          render_dir = self.tmp_path("render")          os.makedirs(render_dir) -        renderer = sysconfig.Renderer() -        renderer.render_network_state(ns, render_dir) +        renderer = self._get_renderer() +        renderer.render_network_state(ns, target=render_dir)          found = dir2dict(render_dir)          nspath = '/etc/sysconfig/network-scripts/'          self.assertNotIn(nspath + 'ifcfg-lo', found.keys()) @@ -1888,8 +2056,8 @@ USERCTL=no          ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK)          render_dir = self.tmp_path("render")          os.makedirs(render_dir) -        renderer = sysconfig.Renderer() -        renderer.render_network_state(ns, render_dir) +        renderer = self._get_renderer() +        renderer.render_network_state(ns, target=render_dir)          found = dir2dict(render_dir)          nspath = '/etc/sysconfig/network-scripts/'          self.assertNotIn(nspath + 'ifcfg-lo', found.keys()) @@ -1906,33 +2074,331 @@ USERCTL=no          self.assertEqual(expected, found[nspath + 'ifcfg-eth0'])      def test_bond_config(self): +        expected_name = 'expected_sysconfig_rhel'          entry = NETWORK_CONFIGS['bond']          found = self._render_and_read(network_config=yaml.load(entry['yaml'])) -        self._compare_files_to_expected(entry['expected_sysconfig'], found) +        self._compare_files_to_expected(entry[expected_name], found) +        self._assert_headers(found) + +    def test_vlan_config(self): +        entry = NETWORK_CONFIGS['vlan'] +        found = self._render_and_read(network_config=yaml.load(entry['yaml'])) +        self._compare_files_to_expected(entry[self.expected_name], found) +        self._assert_headers(found) + +    def test_bridge_config(self): +        entry = NETWORK_CONFIGS['bridge'] +        found = self._render_and_read(network_config=yaml.load(entry['yaml'])) +        self._compare_files_to_expected(entry[self.expected_name], found) +        self._assert_headers(found) + +    def test_manual_config(self): +        entry = NETWORK_CONFIGS['manual'] +        found = self._render_and_read(network_config=yaml.load(entry['yaml'])) +        self._compare_files_to_expected(entry[self.expected_name], found) +        self._assert_headers(found) + +    def test_all_config(self): +        entry = NETWORK_CONFIGS['all'] +        found = self._render_and_read(network_config=yaml.load(entry['yaml'])) +        self._compare_files_to_expected(entry[self.expected_name], found) +        self._assert_headers(found) +        self.assertNotIn( +            'WARNING: Network config: ignoring eth0.101 device-level mtu', +            self.logs.getvalue()) + +    def test_small_config(self): +        entry = NETWORK_CONFIGS['small'] +        found = self._render_and_read(network_config=yaml.load(entry['yaml'])) +        self._compare_files_to_expected(entry[self.expected_name], found) +        self._assert_headers(found) + +    def test_v4_and_v6_static_config(self): +        entry = NETWORK_CONFIGS['v4_and_v6_static'] +        found = self._render_and_read(network_config=yaml.load(entry['yaml'])) +        self._compare_files_to_expected(entry[self.expected_name], found) +        self._assert_headers(found) +        expected_msg = ( +            'WARNING: Network config: ignoring iface0 device-level mtu:8999' +            ' because ipv4 subnet-level mtu:9000 provided.') +        self.assertIn(expected_msg, self.logs.getvalue()) + +    def test_dhcpv6_only_config(self): +        entry = NETWORK_CONFIGS['dhcpv6_only'] +        found = self._render_and_read(network_config=yaml.load(entry['yaml'])) +        self._compare_files_to_expected(entry[self.expected_name], found) +        self._assert_headers(found) + + +class TestOpenSuseSysConfigRendering(CiTestCase): + +    with_logs = True + +    scripts_dir = '/etc/sysconfig/network' +    header = ('# Created by cloud-init on instance boot automatically, ' +              'do not edit.\n#\n') + +    expected_name = 'expected_sysconfig' + +    def _get_renderer(self): +        distro_cls = distros.fetch('opensuse') +        return sysconfig.Renderer( +            config=distro_cls.renderer_configs.get('sysconfig')) + +    def _render_and_read(self, network_config=None, state=None, dir=None): +        if dir is None: +            dir = self.tmp_dir() + +        if network_config: +            ns = network_state.parse_net_config_data(network_config) +        elif state: +            ns = state +        else: +            raise ValueError("Expected data or state, got neither") + +        renderer = self._get_renderer() +        renderer.render_network_state(ns, target=dir) +        return dir2dict(dir) + +    def _compare_files_to_expected(self, expected, found): +        orig_maxdiff = self.maxDiff +        expected_d = dict( +            (os.path.join(self.scripts_dir, k), util.load_shell_content(v)) +            for k, v in expected.items()) + +        # only compare the files in scripts_dir +        scripts_found = dict( +            (k, util.load_shell_content(v)) for k, v in found.items() +            if k.startswith(self.scripts_dir)) +        try: +            self.maxDiff = None +            self.assertEqual(expected_d, scripts_found) +        finally: +            self.maxDiff = orig_maxdiff + +    def _assert_headers(self, found): +        missing = [f for f in found +                   if (f.startswith(self.scripts_dir) and +                       not found[f].startswith(self.header))] +        if missing: +            raise AssertionError("Missing headers in: %s" % missing) + +    @mock.patch("cloudinit.net.sys_dev_path") +    @mock.patch("cloudinit.net.read_sys_net") +    @mock.patch("cloudinit.net.get_devicelist") +    def test_default_generation(self, mock_get_devicelist, +                                mock_read_sys_net, +                                mock_sys_dev_path): +        tmp_dir = self.tmp_dir() +        _setup_test(tmp_dir, mock_get_devicelist, +                    mock_read_sys_net, mock_sys_dev_path) + +        network_cfg = net.generate_fallback_config() +        ns = network_state.parse_net_config_data(network_cfg, +                                                 skip_broken=False) + +        render_dir = os.path.join(tmp_dir, "render") +        os.makedirs(render_dir) + +        renderer = self._get_renderer() +        renderer.render_network_state(ns, target=render_dir) + +        render_file = 'etc/sysconfig/network/ifcfg-eth1000' +        with open(os.path.join(render_dir, render_file)) as fh: +            content = fh.read() +            expected_content = """ +# Created by cloud-init on instance boot automatically, do not edit. +# +BOOTPROTO=dhcp +DEVICE=eth1000 +HWADDR=07-1C-C6-75-A4-BE +NM_CONTROLLED=no +ONBOOT=yes +TYPE=Ethernet +USERCTL=no +""".lstrip() +            self.assertEqual(expected_content, content) + +    def test_multiple_ipv4_default_gateways(self): +        """ValueError is raised when duplicate ipv4 gateways exist.""" +        net_json = { +            "services": [{"type": "dns", "address": "172.19.0.12"}], +            "networks": [{ +                "network_id": "dacd568d-5be6-4786-91fe-750c374b78b4", +                "type": "ipv4", "netmask": "255.255.252.0", +                "link": "tap1a81968a-79", +                "routes": [{ +                    "netmask": "0.0.0.0", +                    "network": "0.0.0.0", +                    "gateway": "172.19.3.254", +                }, { +                    "netmask": "0.0.0.0",  # A second default gateway +                    "network": "0.0.0.0", +                    "gateway": "172.20.3.254", +                }], +                "ip_address": "172.19.1.34", "id": "network0" +            }], +            "links": [ +                { +                    "ethernet_mac_address": "fa:16:3e:ed:9a:59", +                    "mtu": None, "type": "bridge", "id": +                    "tap1a81968a-79", +                    "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f" +                }, +            ], +        } +        macs = {'fa:16:3e:ed:9a:59': 'eth0'} +        render_dir = self.tmp_dir() +        network_cfg = openstack.convert_net_json(net_json, known_macs=macs) +        ns = network_state.parse_net_config_data(network_cfg, +                                                 skip_broken=False) +        renderer = self._get_renderer() +        with self.assertRaises(ValueError): +            renderer.render_network_state(ns, target=render_dir) +        self.assertEqual([], os.listdir(render_dir)) + +    def test_multiple_ipv6_default_gateways(self): +        """ValueError is raised when duplicate ipv6 gateways exist.""" +        net_json = { +            "services": [{"type": "dns", "address": "172.19.0.12"}], +            "networks": [{ +                "network_id": "public-ipv6", +                "type": "ipv6", "netmask": "", +                "link": "tap1a81968a-79", +                "routes": [{ +                    "gateway": "2001:DB8::1", +                    "netmask": "::", +                    "network": "::" +                }, { +                    "gateway": "2001:DB9::1", +                    "netmask": "::", +                    "network": "::" +                }], +                "ip_address": "2001:DB8::10", "id": "network1" +            }], +            "links": [ +                { +                    "ethernet_mac_address": "fa:16:3e:ed:9a:59", +                    "mtu": None, "type": "bridge", "id": +                    "tap1a81968a-79", +                    "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f" +                }, +            ], +        } +        macs = {'fa:16:3e:ed:9a:59': 'eth0'} +        render_dir = self.tmp_dir() +        network_cfg = openstack.convert_net_json(net_json, known_macs=macs) +        ns = network_state.parse_net_config_data(network_cfg, +                                                 skip_broken=False) +        renderer = self._get_renderer() +        with self.assertRaises(ValueError): +            renderer.render_network_state(ns, target=render_dir) +        self.assertEqual([], os.listdir(render_dir)) + +    def test_openstack_rendering_samples(self): +        for os_sample in OS_SAMPLES: +            render_dir = self.tmp_dir() +            ex_input = os_sample['in_data'] +            ex_mac_addrs = os_sample['in_macs'] +            network_cfg = openstack.convert_net_json( +                ex_input, known_macs=ex_mac_addrs) +            ns = network_state.parse_net_config_data(network_cfg, +                                                     skip_broken=False) +            renderer = self._get_renderer() +            # render a multiple times to simulate reboots +            renderer.render_network_state(ns, target=render_dir) +            renderer.render_network_state(ns, target=render_dir) +            renderer.render_network_state(ns, target=render_dir) +            for fn, expected_content in os_sample.get('out_sysconfig_opensuse', +                                                      []): +                with open(os.path.join(render_dir, fn)) as fh: +                    self.assertEqual(expected_content, fh.read()) + +    def test_network_config_v1_samples(self): +        ns = network_state.parse_net_config_data(CONFIG_V1_SIMPLE_SUBNET) +        render_dir = self.tmp_path("render") +        os.makedirs(render_dir) +        renderer = self._get_renderer() +        renderer.render_network_state(ns, target=render_dir) +        found = dir2dict(render_dir) +        nspath = '/etc/sysconfig/network/' +        self.assertNotIn(nspath + 'ifcfg-lo', found.keys()) +        expected = """\ +# Created by cloud-init on instance boot automatically, do not edit. +# +BOOTPROTO=none +DEFROUTE=yes +DEVICE=interface0 +GATEWAY=10.0.2.2 +HWADDR=52:54:00:12:34:00 +IPADDR=10.0.2.15 +NETMASK=255.255.255.0 +NM_CONTROLLED=no +ONBOOT=yes +TYPE=Ethernet +USERCTL=no +""" +        self.assertEqual(expected, found[nspath + 'ifcfg-interface0']) + +    def test_config_with_explicit_loopback(self): +        ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK) +        render_dir = self.tmp_path("render") +        os.makedirs(render_dir) +        renderer = self._get_renderer() +        renderer.render_network_state(ns, target=render_dir) +        found = dir2dict(render_dir) +        nspath = '/etc/sysconfig/network/' +        self.assertNotIn(nspath + 'ifcfg-lo', found.keys()) +        expected = """\ +# Created by cloud-init on instance boot automatically, do not edit. +# +BOOTPROTO=dhcp +DEVICE=eth0 +NM_CONTROLLED=no +ONBOOT=yes +TYPE=Ethernet +USERCTL=no +""" +        self.assertEqual(expected, found[nspath + 'ifcfg-eth0']) + +    def test_bond_config(self): +        expected_name = 'expected_sysconfig_opensuse' +        entry = NETWORK_CONFIGS['bond'] +        found = self._render_and_read(network_config=yaml.load(entry['yaml'])) +        for fname, contents in entry[expected_name].items(): +            print(fname) +            print(contents) +            print() +        print('-- expected ^ | v rendered --') +        for fname, contents in found.items(): +            print(fname) +            print(contents) +            print() +        self._compare_files_to_expected(entry[expected_name], found)          self._assert_headers(found)      def test_vlan_config(self):          entry = NETWORK_CONFIGS['vlan']          found = self._render_and_read(network_config=yaml.load(entry['yaml'])) -        self._compare_files_to_expected(entry['expected_sysconfig'], found) +        self._compare_files_to_expected(entry[self.expected_name], found)          self._assert_headers(found)      def test_bridge_config(self):          entry = NETWORK_CONFIGS['bridge']          found = self._render_and_read(network_config=yaml.load(entry['yaml'])) -        self._compare_files_to_expected(entry['expected_sysconfig'], found) +        self._compare_files_to_expected(entry[self.expected_name], found)          self._assert_headers(found)      def test_manual_config(self):          entry = NETWORK_CONFIGS['manual']          found = self._render_and_read(network_config=yaml.load(entry['yaml'])) -        self._compare_files_to_expected(entry['expected_sysconfig'], found) +        self._compare_files_to_expected(entry[self.expected_name], found)          self._assert_headers(found)      def test_all_config(self):          entry = NETWORK_CONFIGS['all']          found = self._render_and_read(network_config=yaml.load(entry['yaml'])) -        self._compare_files_to_expected(entry['expected_sysconfig'], found) +        self._compare_files_to_expected(entry[self.expected_name], found)          self._assert_headers(found)          self.assertNotIn(              'WARNING: Network config: ignoring eth0.101 device-level mtu', @@ -1941,13 +2407,13 @@ USERCTL=no      def test_small_config(self):          entry = NETWORK_CONFIGS['small']          found = self._render_and_read(network_config=yaml.load(entry['yaml'])) -        self._compare_files_to_expected(entry['expected_sysconfig'], found) +        self._compare_files_to_expected(entry[self.expected_name], found)          self._assert_headers(found)      def test_v4_and_v6_static_config(self):          entry = NETWORK_CONFIGS['v4_and_v6_static']          found = self._render_and_read(network_config=yaml.load(entry['yaml'])) -        self._compare_files_to_expected(entry['expected_sysconfig'], found) +        self._compare_files_to_expected(entry[self.expected_name], found)          self._assert_headers(found)          expected_msg = (              'WARNING: Network config: ignoring iface0 device-level mtu:8999' @@ -1957,7 +2423,7 @@ USERCTL=no      def test_dhcpv6_only_config(self):          entry = NETWORK_CONFIGS['dhcpv6_only']          found = self._render_and_read(network_config=yaml.load(entry['yaml'])) -        self._compare_files_to_expected(entry['expected_sysconfig'], found) +        self._compare_files_to_expected(entry[self.expected_name], found)          self._assert_headers(found) @@ -1982,7 +2448,7 @@ class TestEniNetRendering(CiTestCase):          renderer = eni.Renderer(              {'eni_path': 'interfaces', 'netrules_path': None}) -        renderer.render_network_state(ns, render_dir) +        renderer.render_network_state(ns, target=render_dir)          self.assertTrue(os.path.exists(os.path.join(render_dir,                                                      'interfaces'))) @@ -2002,7 +2468,7 @@ iface eth1000 inet dhcp          tmp_dir = self.tmp_dir()          ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK)          renderer = eni.Renderer() -        renderer.render_network_state(ns, tmp_dir) +        renderer.render_network_state(ns, target=tmp_dir)          expected = """\  auto lo  iface lo inet loopback @@ -2038,7 +2504,7 @@ class TestNetplanNetRendering(CiTestCase):          render_target = 'netplan.yaml'          renderer = netplan.Renderer(              {'netplan_path': render_target, 'postcmds': False}) -        renderer.render_network_state(ns, render_dir) +        renderer.render_network_state(ns, target=render_dir)          self.assertTrue(os.path.exists(os.path.join(render_dir,                                                      render_target))) @@ -2143,7 +2609,7 @@ class TestNetplanPostcommands(CiTestCase):          render_target = 'netplan.yaml'          renderer = netplan.Renderer(              {'netplan_path': render_target, 'postcmds': True}) -        renderer.render_network_state(ns, render_dir) +        renderer.render_network_state(ns, target=render_dir)          mock_netplan_generate.assert_called_with(run=True)          mock_net_setup_link.assert_called_with(run=True) @@ -2168,7 +2634,7 @@ class TestNetplanPostcommands(CiTestCase):                         '/sys/class/net/lo'], capture=True),          ]          with mock.patch.object(os.path, 'islink', return_value=True): -            renderer.render_network_state(ns, render_dir) +            renderer.render_network_state(ns, target=render_dir)              mock_subp.assert_has_calls(expected) @@ -2363,7 +2829,7 @@ class TestNetplanRoundTrip(CiTestCase):          renderer = netplan.Renderer(              config={'netplan_path': netplan_path}) -        renderer.render_network_state(ns, target) +        renderer.render_network_state(ns, target=target)          return dir2dict(target)      def testsimple_render_bond_netplan(self): @@ -2453,7 +2919,7 @@ class TestEniRoundTrip(CiTestCase):          renderer = eni.Renderer(              config={'eni_path': eni_path, 'netrules_path': netrules_path}) -        renderer.render_network_state(ns, dir) +        renderer.render_network_state(ns, target=dir)          return dir2dict(dir)      def testsimple_convert_and_render(self): | 
