← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~goneri/cloud-init:freebsd_net_renderer into cloud-init:master

 

Gonéri Le Bouder has proposed merging ~goneri/cloud-init:freebsd_net_renderer into cloud-init:master.

Requested reviews:
  cloud-init commiters (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~goneri/cloud-init/+git/cloud-init/+merge/365641
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~goneri/cloud-init:freebsd_net_renderer into cloud-init:master.
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index 20c994d..c453234 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -145,7 +145,7 @@ class Distro(object):
         # Write it out
 
         # pylint: disable=assignment-from-no-return
-        # We have implementations in arch, freebsd and gentoo still
+        # We have implementations in arch and gentoo still
         dev_names = self._write_network(settings)
         # pylint: enable=assignment-from-no-return
         # Now try to bring them up
diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py
index ff22d56..2ab11a5 100644
--- a/cloudinit/distros/freebsd.py
+++ b/cloudinit/distros/freebsd.py
@@ -15,10 +15,6 @@ from cloudinit import helpers
 from cloudinit import log as logging
 from cloudinit import ssh_util
 from cloudinit import util
-
-from cloudinit.distros import net_util
-from cloudinit.distros.parsers.resolv_conf import ResolvConf
-
 from cloudinit.settings import PER_INSTANCE
 
 LOG = logging.getLogger(__name__)
@@ -274,309 +270,8 @@ class Distro(distros.Distro):
             keys = set(kwargs['ssh_authorized_keys']) or []
             ssh_util.setup_user_keys(keys, name, options=None)
 
-    @staticmethod
-    def get_ifconfig_list():
-        cmd = ['ifconfig', '-l']
-        (nics, err) = util.subp(cmd, rcs=[0, 1])
-        if len(err):
-            LOG.warning("Error running %s: %s", cmd, err)
-            return None
-        return nics
-
-    @staticmethod
-    def get_ifconfig_ifname_out(ifname):
-        cmd = ['ifconfig', ifname]
-        (if_result, err) = util.subp(cmd, rcs=[0, 1])
-        if len(err):
-            LOG.warning("Error running %s: %s", cmd, err)
-            return None
-        return if_result
-
-    @staticmethod
-    def get_ifconfig_ether():
-        cmd = ['ifconfig', '-l', 'ether']
-        (nics, err) = util.subp(cmd, rcs=[0, 1])
-        if len(err):
-            LOG.warning("Error running %s: %s", cmd, err)
-            return None
-        return nics
-
-    @staticmethod
-    def get_interface_mac(ifname):
-        if_result = Distro.get_ifconfig_ifname_out(ifname)
-        for item in if_result.splitlines():
-            if item.find('ether ') != -1:
-                mac = str(item.split()[1])
-                if mac:
-                    return mac
-
-    @staticmethod
-    def get_devicelist():
-        nics = Distro.get_ifconfig_list()
-        return nics.split()
-
-    @staticmethod
-    def get_ipv6():
-        ipv6 = []
-        nics = Distro.get_devicelist()
-        for nic in nics:
-            if_result = Distro.get_ifconfig_ifname_out(nic)
-            for item in if_result.splitlines():
-                if item.find("inet6 ") != -1 and item.find("scopeid") == -1:
-                    ipv6.append(nic)
-        return ipv6
-
-    def get_ipv4(self):
-        ipv4 = []
-        nics = Distro.get_devicelist()
-        for nic in nics:
-            if_result = Distro.get_ifconfig_ifname_out(nic)
-            for item in if_result.splitlines():
-                print(item)
-                if self.ipv4_pat.match(item):
-                    ipv4.append(nic)
-        return ipv4
-
-    def is_up(self, ifname):
-        if_result = Distro.get_ifconfig_ifname_out(ifname)
-        pat = "^" + ifname
-        for item in if_result.splitlines():
-            if re.match(pat, item):
-                flags = item.split('<')[1].split('>')[0]
-                if flags.find("UP") != -1:
-                    return True
-
-    def _get_current_rename_info(self, check_downable=True):
-        """Collect information necessary for rename_interfaces."""
-        names = Distro.get_devicelist()
-        bymac = {}
-        for n in names:
-            bymac[Distro.get_interface_mac(n)] = {
-                'name': n, 'up': self.is_up(n), 'downable': None}
-
-        nics_with_addresses = set()
-        if check_downable:
-            nics_with_addresses = set(self.get_ipv4() + self.get_ipv6())
-
-        for d in bymac.values():
-            d['downable'] = (d['up'] is False or
-                             d['name'] not in nics_with_addresses)
-
-        return bymac
-
-    def _rename_interfaces(self, renames):
-        if not len(renames):
-            LOG.debug("no interfaces to rename")
-            return
-
-        current_info = self._get_current_rename_info()
-
-        cur_bymac = {}
-        for mac, data in current_info.items():
-            cur = data.copy()
-            cur['mac'] = mac
-            cur_bymac[mac] = cur
-
-        def update_byname(bymac):
-            return dict((data['name'], data)
-                        for data in bymac.values())
-
-        def rename(cur, new):
-            util.subp(["ifconfig", cur, "name", new], capture=True)
-
-        def down(name):
-            util.subp(["ifconfig", name, "down"], capture=True)
-
-        def up(name):
-            util.subp(["ifconfig", name, "up"], capture=True)
-
-        ops = []
-        errors = []
-        ups = []
-        cur_byname = update_byname(cur_bymac)
-        tmpname_fmt = "cirename%d"
-        tmpi = -1
-
-        for mac, new_name in renames:
-            cur = cur_bymac.get(mac, {})
-            cur_name = cur.get('name')
-            cur_ops = []
-            if cur_name == new_name:
-                # nothing to do
-                continue
-
-            if not cur_name:
-                errors.append("[nic not present] Cannot rename mac=%s to %s"
-                              ", not available." % (mac, new_name))
-                continue
-
-            if cur['up']:
-                msg = "[busy] Error renaming mac=%s from %s to %s"
-                if not cur['downable']:
-                    errors.append(msg % (mac, cur_name, new_name))
-                    continue
-                cur['up'] = False
-                cur_ops.append(("down", mac, new_name, (cur_name,)))
-                ups.append(("up", mac, new_name, (new_name,)))
-
-            if new_name in cur_byname:
-                target = cur_byname[new_name]
-                if target['up']:
-                    msg = "[busy-target] Error renaming mac=%s from %s to %s."
-                    if not target['downable']:
-                        errors.append(msg % (mac, cur_name, new_name))
-                        continue
-                    else:
-                        cur_ops.append(("down", mac, new_name, (new_name,)))
-
-                tmp_name = None
-                while tmp_name is None or tmp_name in cur_byname:
-                    tmpi += 1
-                    tmp_name = tmpname_fmt % tmpi
-
-                cur_ops.append(("rename", mac, new_name, (new_name, tmp_name)))
-                target['name'] = tmp_name
-                cur_byname = update_byname(cur_bymac)
-                if target['up']:
-                    ups.append(("up", mac, new_name, (tmp_name,)))
-
-            cur_ops.append(("rename", mac, new_name, (cur['name'], new_name)))
-            cur['name'] = new_name
-            cur_byname = update_byname(cur_bymac)
-            ops += cur_ops
-
-        opmap = {'rename': rename, 'down': down, 'up': up}
-        if len(ops) + len(ups) == 0:
-            if len(errors):
-                LOG.debug("unable to do any work for renaming of %s", renames)
-            else:
-                LOG.debug("no work necessary for renaming of %s", renames)
-        else:
-            LOG.debug("achieving renaming of %s with ops %s",
-                      renames, ops + ups)
-
-            for op, mac, new_name, params in ops + ups:
-                try:
-                    opmap.get(op)(*params)
-                except Exception as e:
-                    errors.append(
-                        "[unknown] Error performing %s%s for %s, %s: %s" %
-                        (op, params, mac, new_name, e))
-        if len(errors):
-            raise Exception('\n'.join(errors))
-
-    def apply_network_config_names(self, netcfg):
-        renames = []
-        for ent in netcfg.get('config', {}):
-            if ent.get('type') != 'physical':
-                continue
-            mac = ent.get('mac_address')
-            name = ent.get('name')
-            if not mac:
-                continue
-            renames.append([mac, name])
-        return self._rename_interfaces(renames)
-
-    @classmethod
-    def generate_fallback_config(self):
-        nics = Distro.get_ifconfig_ether()
-        if nics is None:
-            LOG.debug("Fail to get network interfaces")
-            return None
-        potential_interfaces = nics.split()
-        connected = []
-        for nic in potential_interfaces:
-            pat = "^" + nic
-            if_result = Distro.get_ifconfig_ifname_out(nic)
-            for item in if_result.split("\n"):
-                if re.match(pat, item):
-                    flags = item.split('<')[1].split('>')[0]
-                    if flags.find("RUNNING") != -1:
-                        connected.append(nic)
-        if connected:
-            potential_interfaces = connected
-        names = list(sorted(potential_interfaces))
-        default_pri_nic = Distro.default_primary_nic
-        if default_pri_nic in names:
-            names.remove(default_pri_nic)
-            names.insert(0, default_pri_nic)
-        target_name = None
-        target_mac = None
-        for name in names:
-            mac = Distro.get_interface_mac(name)
-            if mac:
-                target_name = name
-                target_mac = mac
-                break
-        if target_mac and target_name:
-            nconf = {'config': [], 'version': 1}
-            nconf['config'].append(
-                {'type': 'physical', 'name': target_name,
-                 'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]})
-            return nconf
-        else:
-            return None
-
-    def _write_network(self, settings):
-        entries = net_util.translate_network(settings)
-        nameservers = []
-        searchdomains = []
-        dev_names = entries.keys()
-        for (device, info) in entries.items():
-            # Skip the loopback interface.
-            if device.startswith('lo'):
-                continue
-
-            dev = self.getnetifname(device)
-
-            LOG.info('Configuring interface %s', dev)
-
-            if info.get('bootproto') == 'static':
-                LOG.debug('Configuring dev %s with %s / %s', dev,
-                          info.get('address'), info.get('netmask'))
-                # Configure an ipv4 address.
-                ifconfig = (info.get('address') + ' netmask ' +
-                            info.get('netmask'))
-
-                # Configure the gateway.
-                self.updatercconf('defaultrouter', info.get('gateway'))
-
-                if 'dns-nameservers' in info:
-                    nameservers.extend(info['dns-nameservers'])
-                if 'dns-search' in info:
-                    searchdomains.extend(info['dns-search'])
-            else:
-                ifconfig = 'DHCP'
-
-            self.updatercconf('ifconfig_' + dev, ifconfig)
-
-        # Try to read the /etc/resolv.conf or just start from scratch if that
-        # fails.
-        try:
-            resolvconf = ResolvConf(util.load_file(self.resolv_conf_fn))
-            resolvconf.parse()
-        except IOError:
-            util.logexc(LOG, "Failed to parse %s, use new empty file",
-                        self.resolv_conf_fn)
-            resolvconf = ResolvConf('')
-            resolvconf.parse()
-
-        # Add some nameservers
-        for server in nameservers:
-            try:
-                resolvconf.add_nameserver(server)
-            except ValueError:
-                util.logexc(LOG, "Failed to add nameserver %s", server)
-
-        # And add any searchdomains.
-        for domain in searchdomains:
-            try:
-                resolvconf.add_search_domain(domain)
-            except ValueError:
-                util.logexc(LOG, "Failed to add search domain %s", domain)
-        util.write_file(self.resolv_conf_fn, str(resolvconf), 0o644)
-
-        return dev_names
+    def _write_network_config(self, netconfig):
+        return self._supported_write_network_config(netconfig)
 
     def apply_locale(self, locale, out_fn=None):
         # Adjust the locals value to the new value
@@ -604,18 +299,9 @@ class Distro(distros.Distro):
                 util.logexc(LOG, "Failed to restore %s backup",
                             self.login_conf_fn)
 
-    def _bring_up_interface(self, device_name):
-        if device_name.startswith('lo'):
-            return
-        dev = self.getnetifname(device_name)
-        cmd = ['/etc/rc.d/netif', 'start', dev]
-        LOG.debug("Attempting to bring up interface %s using command %s",
-                  dev, cmd)
-        # This could return 1 when the interface has already been put UP by the
-        # OS. This is just fine.
-        (_out, err) = util.subp(cmd, rcs=[0, 1])
-        if len(err):
-            LOG.warning("Error running %s: %s", cmd, err)
+    def apply_network_config_names(self, netconfig):
+        # This is handled by the freebsd network renderer.
+        return
 
     def install_packages(self, pkglist):
         self.update_package_sources()
diff --git a/cloudinit/net/freebsd.py b/cloudinit/net/freebsd.py
new file mode 100644
index 0000000..595541c
--- /dev/null
+++ b/cloudinit/net/freebsd.py
@@ -0,0 +1,190 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import os
+import re
+from six import StringIO
+
+from cloudinit import log as logging
+from cloudinit import util
+from cloudinit.distros.parsers.resolv_conf import ResolvConf
+
+from . import renderer
+
+LOG = logging.getLogger(__name__)
+
+
+class Renderer(renderer.Renderer):
+    rc_conf_fn = '/etc/rc.conf'
+    resolv_conf_fn = '/etc/resolv.conf'
+
+    # Updates a key in /etc/rc.conf.
+    # TODO(Goneri): Duplicated with distros.freebsd
+    def updatercconf(self, key, value):
+        LOG.debug("Checking %s for: %s = %s", self.rc_conf_fn, key, value)
+        conf = self.loadrcconf()
+        config_changed = False
+        if key not in conf:
+            LOG.debug("Adding key in %s: %s = %s", self.rc_conf_fn, key,
+                      value)
+            conf[key] = value
+            config_changed = True
+        else:
+            for item in conf.keys():
+                if item == key and conf[item] != value:
+                    conf[item] = value
+                    LOG.debug("Changing key in %s: %s = %s", self.rc_conf_fn,
+                              key, value)
+                    config_changed = True
+
+        if config_changed:
+            LOG.info("Writing %s", self.rc_conf_fn)
+            buf = StringIO()
+            for keyval in conf.items():
+                buf.write('%s="%s"\n' % keyval)
+            util.write_file(self.rc_conf_fn, buf.getvalue())
+
+    # Load the contents of /etc/rc.conf and store all keys in a dict. Make sure
+    # quotes are ignored:
+    #  hostname="bla"
+    # TODO(Goneri): Duplicated with distros.freebsd
+    def loadrcconf(self):
+        RE_MATCH = re.compile(r'^(\w+)\s*=\s*(.*)\s*')
+        conf = {}
+        lines = util.load_file(self.rc_conf_fn).splitlines()
+        for line in lines:
+            m = RE_MATCH.match(line)
+            if not m:
+                LOG.debug("Skipping line from /etc/rc.conf: %s", line)
+                continue
+            key = m.group(1).rstrip()
+            val = m.group(2).rstrip()
+            # Kill them quotes (not completely correct, aka won't handle
+            # quoted values, but should be ok ...)
+            if val[0] in ('"', "'"):
+                val = val[1:]
+            if val[-1] in ('"', "'"):
+                val = val[0:-1]
+            if len(val) == 0:
+                LOG.debug("Skipping empty value from /etc/rc.conf: %s", line)
+                continue
+            conf[key] = val
+        return conf
+
+    # TODO(Goneri): Duplicated with distros.freebsd
+    def readrcconf(self, key):
+        conf = self.loadrcconf()
+        try:
+            val = conf[key]
+        except KeyError:
+            val = None
+        return val
+
+    def __init__(self, config=None):
+        if not config:
+            config = {}
+        self.rcconf_path = config.get('rcconf_path', 'etc/rc.conf')
+
+    def _render_route(self, route, indent=""):
+        pass
+
+    def _render_iface(self, iface, render_hwaddress=False):
+        pass
+
+    def _ifconfig_a(self):
+        (out, _) = util.subp(['ifconfig', '-a'])
+        return out
+
+    def _get_ifname_by_mac(self, mac):
+        out = self._ifconfig_a()
+        blocks = re.split(r'(^\S+|\n\S+):', out)
+        blocks.reverse()
+        blocks.pop()  # Ignore the first one
+        while blocks:
+            ifname = blocks.pop()
+            m = re.search(r'ether\s([\da-f:]{17})', blocks.pop())
+            if m and m.group(1) == mac:
+                return ifname
+
+    def _write_network(self, settings):
+        nameservers = []
+        searchdomains = []
+        for interface in settings.iter_interfaces():
+            device_name = interface.get("name")
+            device_mac = interface.get("mac_address")
+            if device_name:
+                if re.match(r'^lo\d+$', device_name):
+                    continue
+            if device_mac and device_name:
+                cur_name = self._get_ifname_by_mac(device_mac)
+                if not cur_name:
+                    LOG.info('Cannot find any device with MAC %s', device_mac)
+                    continue
+                if cur_name != device_name:
+                    self.updatercconf(
+                        'ifconfig_%s_name' % cur_name,
+                        device_name)
+            elif device_mac:
+                device_name = self._get_ifname_by_mac(device_mac)
+
+            subnet = interface.get("subnets", [])[0]
+            LOG.info('Configuring interface %s', device_name)
+
+            if subnet.get('type') == 'static':
+                LOG.debug('Configuring dev %s with %s / %s', device_name,
+                          subnet.get('address'), subnet.get('netmask'))
+                # Configure an ipv4 address.
+                ifconfig = (subnet.get('address') + ' netmask ' +
+                            subnet.get('netmask'))
+
+                # Configure the gateway.
+                self.updatercconf('defaultrouter', subnet.get('gateway'))
+
+                if 'dns_nameservers' in subnet:
+                    nameservers.extend(subnet['dns_nameservers'])
+                if 'dns_search' in subnet:
+                    searchdomains.extend(subnet['dns_search'])
+            else:
+                ifconfig = 'DHCP'
+
+            self.updatercconf('ifconfig_' + device_name, ifconfig)
+        # Note: We don't try to be cleaver beceause if an interface
+        # is renamed, we must reload the netif.
+        util.subp(['/etc/rc.d/netif', 'restart'])
+        util.subp(['/etc/rc.d/routing', 'restart'])
+
+        # Try to read the /etc/resolv.conf or just start from scratch if that
+        # fails.
+        try:
+            resolvconf = ResolvConf(util.load_file(self.resolv_conf_fn))
+            resolvconf.parse()
+        except IOError:
+            util.logexc(LOG, "Failed to parse %s, use new empty file",
+                        self.resolv_conf_fn)
+            resolvconf = ResolvConf('')
+            resolvconf.parse()
+
+        # Add some nameservers
+        for server in nameservers:
+            try:
+                resolvconf.add_nameserver(server)
+            except ValueError:
+                util.logexc(LOG, "Failed to add nameserver %s", server)
+
+        # And add any searchdomains.
+        for domain in searchdomains:
+            try:
+                resolvconf.add_search_domain(domain)
+            except ValueError:
+                util.logexc(LOG, "Failed to add search domain %s", domain)
+        util.write_file(self.resolv_conf_fn, str(resolvconf), 0o644)
+
+    def render_network_state(self, network_state, templates=None, target=None):
+        self._write_network(network_state)
+
+
+def available(target=None):
+    rcconf_path = util.target_path(target, 'etc/rc.conf')
+    if not os.path.isfile(rcconf_path):
+        return False
+
+    return True
diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py
index 5117b4a..b98dbbe 100644
--- a/cloudinit/net/renderers.py
+++ b/cloudinit/net/renderers.py
@@ -1,17 +1,19 @@
 # This file is part of cloud-init. See LICENSE file for license information.
 
 from . import eni
+from . import freebsd
 from . import netplan
 from . import RendererNotFoundError
 from . import sysconfig
 
 NAME_TO_RENDERER = {
     "eni": eni,
+    "freebsd": freebsd,
     "netplan": netplan,
     "sysconfig": sysconfig,
 }
 
-DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan"]
+DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan", "freebsd"]
 
 
 def search(priority=None, target=None, first=False):
diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst
index 1e99455..6620bcb 100644
--- a/doc/rtd/topics/network-config.rst
+++ b/doc/rtd/topics/network-config.rst
@@ -190,7 +190,7 @@ supplying an updated configuration in cloud-config. ::
 
   system_info:
     network:
-      renderers: ['netplan', 'eni', 'sysconfig']
+      renderers: ['netplan', 'eni', 'sysconfig', 'freebsd']
 
 
 Network Configuration Tools
diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
index c3c0c8c..7b29f71 100644
--- a/tests/unittests/test_distros/test_netconfig.py
+++ b/tests/unittests/test_distros/test_netconfig.py
@@ -1,5 +1,6 @@
 # This file is part of cloud-init. See LICENSE file for license information.
 
+import copy
 import os
 from six import StringIO
 from textwrap import dedent
@@ -213,128 +214,127 @@ class TestNetCfgDistroBase(FilesystemMockingTestCase):
             self.assertEqual(v, b2[k])
 
 
-class TestNetCfgDistroFreebsd(TestNetCfgDistroBase):
+class TestNetCfgDistroFreeBSD(TestNetCfgDistroBase):
 
-    frbsd_ifout = """\
-hn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
+    def setUp(self):
+        super(TestNetCfgDistroFreeBSD, self).setUp()
+        self.distro = self._get_distro('freebsd', renderers=['freebsd'])
+
+    def _apply_and_verify_freebsd(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.freebsd.available') as m_avail:
+            m_avail.return_value = True
+            with self.reRooted(tmpd) as tmpd:
+                util.ensure_dir('/etc')
+                util.ensure_file('/etc/rc.conf')
+                util.ensure_file('/etc/resolv.conf')
+                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))
+
+    @mock.patch('cloudinit.net.freebsd.Renderer._ifconfig_a')
+    def test_apply_network_config_freebsd_standard(self, ifconfig_a):
+        ifconfig_a.return_value = """\
+eth0: 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
+
+eth1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
+        options=6c07bb<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,JUMBO_MTU,VLAN_HWCSUM,TSO4,TSO6,LRO,VLAN_HWTSO,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
+        ether 52:54:00:0e:33:89
+        media: Ethernet 10Gbase-T <full-duplex>
+        status: active
+
+lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
+        options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
+        inet6 ::1 prefixlen 128
+        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x2
+        inet 127.0.0.1 netmask 0xff000000
+        groups: lo
+        nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL>
+"""
+        rc_conf_expected = """\
+defaultrouter="192.168.1.254"
+ifconfig_eth0="192.168.1.5 netmask 255.255.255.0"
+ifconfig_eth1="DHCP"
 """
 
-    @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):
-        frbsd_distro = self._get_distro('freebsd')
-        iflist.return_value = "lo0 hn0"
-        ifname_out.return_value = self.frbsd_ifout
-        res = frbsd_distro.get_ipv4()
-        self.assertEqual(res, ['lo0', 'hn0'])
-        res = frbsd_distro.get_ipv6()
-        self.assertEqual(res, [])
-
-    @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ether')
-    @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ifname_out')
-    @mock.patch('cloudinit.distros.freebsd.Distro.get_interface_mac')
-    def test_generate_fallback_config_freebsd(self, mac, ifname_out, if_ether):
-        frbsd_distro = self._get_distro('freebsd')
-
-        if_ether.return_value = 'hn0'
-        ifname_out.return_value = self.frbsd_ifout
-        mac.return_value = '00:15:5d:4c:73:00'
-        res = frbsd_distro.generate_fallback_config()
-        self.assertIsNotNone(res)
-
-    def test_simple_write_freebsd(self):
-        fbsd_distro = self._get_distro('freebsd')
-
-        rc_conf = '/etc/rc.conf'
-        read_bufs = {
-            rc_conf: 'initial-rc-conf-not-validated',
-            '/etc/resolv.conf': 'initial-resolv-conf-not-validated',
+        expected_cfgs = {
+            '/etc/rc.conf': rc_conf_expected,
+            '/etc/resolv.conf': ''
         }
+        self._apply_and_verify_freebsd(self.distro.apply_network_config,
+                                       V1_NET_CFG,
+                                       expected_cfgs=expected_cfgs.copy())
 
-        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_simple_write_freebsd_from_v2eni(self):
-        fbsd_distro = self._get_distro('freebsd')
-
-        rc_conf = '/etc/rc.conf'
-        read_bufs = {
-            rc_conf: 'initial-rc-conf-not-validated',
-            '/etc/resolv.conf': 'initial-resolv-conf-not-validated',
-        }
 
-        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_FROM_V2, 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
-        # of _write_network_config or apply_network_config in fbsd now,
-        # which would make this test not actually test the fallback.
-        self.assertRaises(
-            NotImplementedError, fbsd_distro._write_network_config,
-            BASE_NET_CFG)
-
-        # now run
-        mynetcfg = {
-            'config': [{"type": "physical", "name": "eth0",
-                        "mac_address": "c0:d6:9f:2c:e8:80",
-                        "subnets": [{"type": "dhcp"}]}],
-            'version': 1}
-
-        rc_conf = '/etc/rc.conf'
-        read_bufs = {
-            rc_conf: 'initial-rc-conf-not-validated',
-            '/etc/resolv.conf': 'initial-resolv-conf-not-validated',
+    @mock.patch('cloudinit.net.freebsd.Renderer._ifconfig_a')
+    def test_apply_network_config_freebsd_ifrename(self, ifconfig_a):
+        ifconfig_a.return_value = """\
+vtnet0: 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
+        media: Ethernet autoselect (10Gbase-T <full-duplex>)
+        status: active
+
+lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
+        options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
+        inet6 ::1 prefixlen 128
+        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x2
+        inet 127.0.0.1 netmask 0xff000000
+        groups: lo
+        nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL>
+"""
+        rc_conf_expected = """\
+ifconfig_vtnet0_name="eth0"
+defaultrouter="192.168.1.254"
+ifconfig_eth0="192.168.1.5 netmask 255.255.255.0"
+ifconfig_eth1="DHCP"
+"""
+
+        V1_NET_CFG_RENAME = copy.deepcopy(V1_NET_CFG)
+        V1_NET_CFG_RENAME['config'][0]['mac_address'] = '00:15:5d:4c:73:00'
+
+        expected_cfgs = {
+            '/etc/rc.conf': rc_conf_expected,
+            '/etc/resolv.conf': ''
         }
+        self._apply_and_verify_freebsd(self.distro.apply_network_config,
+                                       V1_NET_CFG_RENAME,
+                                       expected_cfgs=expected_cfgs.copy())
 
-        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)
-
-        self.assertIn(rc_conf, results)
-        self.assertCfgEquals('ifconfig_vtnet0="DHCP"', results[rc_conf])
-        self.assertEqual(0o644, get_mode(rc_conf, tmpd))
+    @mock.patch('cloudinit.net.freebsd.Renderer._ifconfig_a')
+    def test_apply_network_config_freebsd_nameserver(self, ifconfig_a):
+        ifconfig_a.return_value = """\
+eth0: 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
+        media: Ethernet autoselect (10Gbase-T <full-duplex>)
+        status: active
+"""
+
+        V1_NET_CFG_DNS = copy.deepcopy(V1_NET_CFG)
+        V1_NET_CFG_DNS['config'][0]['subnets'][0]['dns_nameservers'] = ['1.2.3.4']
+        expected_cfgs = {
+            '/etc/resolv.conf': 'nameserver 1.2.3.4\n'
+        }
+        self._apply_and_verify_freebsd(self.distro.apply_network_config,
+                                       V1_NET_CFG_DNS,
+                                       expected_cfgs=expected_cfgs.copy())
 
 
 class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase):