← Back to team overview

cloud-init-dev team mailing list archive

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

 

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

Commit message:
NetBSD support

Add support for the NetBSD Operating System. This branch has been tested
with:

- a NoCloud data source
- and NetBSD 8.0 and 8.1.

This commit depends on the following merge requests:

- https://code.launchpad.net/~goneri/cloud-init/+git/cloud-init/+merge/365641
- https://code.launchpad.net/~goneri/cloud-init/+git/cloud-init/+merge/368507

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

For more details, see:
https://code.launchpad.net/~goneri/cloud-init/+git/cloud-init/+merge/368508
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~goneri/cloud-init:netbsd into cloud-init:master.
diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py
index 4585e4d..90ea460 100755
--- a/cloudinit/config/cc_set_passwords.py
+++ b/cloudinit/config/cc_set_passwords.py
@@ -164,35 +164,32 @@ def handle(_name, cfg, cloud, log, args):
         for line in plist:
             u, p = line.split(':', 1)
             if prog.match(p) is not None and ":" not in p:
-                hashed_plist_in.append("%s:%s" % (u, p))
+                hashed_plist_in.append((u, p))
                 hashed_users.append(u)
             else:
                 if p == "R" or p == "RANDOM":
                     p = rand_user_password()
                     randlist.append("%s:%s" % (u, p))
-                plist_in.append("%s:%s" % (u, p))
+                plist_in.append((u, p))
                 users.append(u)
 
-        ch_in = '\n'.join(plist_in) + '\n'
         if users:
             try:
                 log.debug("Changing password for %s:", users)
-                util.subp(['chpasswd'], ch_in)
+                cloud.distro.user_passwords(plist_in, 'clear')
             except Exception as e:
                 errors.append(e)
                 util.logexc(
-                    log, "Failed to set passwords with chpasswd for %s", users)
+                    log, "Failed to set passwords for %s", users)
 
-        hashed_ch_in = '\n'.join(hashed_plist_in) + '\n'
         if hashed_users:
             try:
                 log.debug("Setting hashed password for %s:", hashed_users)
-                util.subp(['chpasswd', '-e'], hashed_ch_in)
+                cloud.distro.user_passwords(hashed_plist_in, 'hashed')
             except Exception as e:
                 errors.append(e)
                 util.logexc(
-                    log, "Failed to set hashed passwords with chpasswd for %s",
-                    hashed_users)
+                    log, "Failed to set hashed passwords for %s", hashed_users)
 
         if len(randlist):
             blurb = ("Set the following 'random' passwords\n",
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index 20c994d..e3c36e6 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
@@ -715,6 +715,18 @@ class Distro(object):
                 util.subp(['usermod', '-a', '-G', name, member])
                 LOG.info("Added user '%s' to group '%s'", member, name)
 
+    def user_passwords(self, entries, format):
+        ch_in = ""
+        for i in entries:
+            user, password = i
+            ch_in += "%s:%s\n" % (user, password)
+        if format == 'clear':
+            util.subp(['chpasswd'], ch_in)
+        elif format == 'hashed':
+            util.subp(['chpasswd', '-e'], ch_in)
+        else:
+            LOG.warning("user_passwords: unexpected format: %s", format)
+
 
 def _get_package_mirror_info(mirror_info, data_source=None,
                              mirror_filter=util.search_for_mirror):
diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py
index ff22d56..605afd3 100644
--- a/cloudinit/distros/freebsd.py
+++ b/cloudinit/distros/freebsd.py
@@ -15,22 +15,16 @@ 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.distros import rhel_util
 from cloudinit.settings import PER_INSTANCE
 
 LOG = logging.getLogger(__name__)
 
 
 class Distro(distros.Distro):
-    rc_conf_fn = "/etc/rc.conf"
     login_conf_fn = '/etc/login.conf'
     login_conf_fn_bak = '/etc/login.conf.orig'
-    resolv_conf_fn = '/etc/resolv.conf'
     ci_sudoers_fn = '/usr/local/etc/sudoers.d/90-cloud-init-users'
-    default_primary_nic = 'hn0'
 
     def __init__(self, name, cfg, paths):
         distros.Distro.__init__(self, name, cfg, paths)
@@ -39,99 +33,8 @@ class Distro(distros.Distro):
         # should only happen say once per instance...)
         self._runner = helpers.Runners(paths)
         self.osfamily = 'freebsd'
-        self.ipv4_pat = re.compile(r"\s+inet\s+\d+[.]\d+[.]\d+[.]\d+")
         cfg['ssh_svcname'] = 'sshd'
 
-    # Updates a key in /etc/rc.conf.
-    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"
-    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
-
-    def readrcconf(self, key):
-        conf = self.loadrcconf()
-        try:
-            val = conf[key]
-        except KeyError:
-            val = None
-        return val
-
-    # NOVA will inject something like eth0, rewrite that to use the FreeBSD
-    # adapter. Since this adapter is based on the used driver, we need to
-    # figure out which interfaces are available. On KVM platforms this is
-    # vtnet0, where Xen would use xn0.
-    def getnetifname(self, dev):
-        LOG.debug("Translating network interface %s", dev)
-        if dev.startswith('lo'):
-            return dev
-
-        n = re.search(r'\d+$', dev)
-        index = n.group(0)
-
-        (out, _err) = util.subp(['ifconfig', '-a'])
-        ifconfigoutput = [x for x in (out.strip()).splitlines()
-                          if len(x.split()) > 0]
-        bsddev = 'NOT_FOUND'
-        for line in ifconfigoutput:
-            m = re.match(r'^\w+', line)
-            if m:
-                if m.group(0).startswith('lo'):
-                    continue
-                # Just settle with the first non-lo adapter we find, since it's
-                # rather unlikely there will be multiple nicdrivers involved.
-                bsddev = m.group(0)
-                break
-
-        # Replace the index with the one we're after.
-        bsddev = re.sub(r'\d+$', index, bsddev)
-        LOG.debug("Using network interface %s", bsddev)
-        return bsddev
-
     def _select_hostname(self, hostname, fqdn):
         # Should be FQDN if available. See rc.conf(5) in FreeBSD
         if fqdn:
@@ -143,17 +46,16 @@ class Distro(distros.Distro):
         return ('rc.conf', sys_hostname)
 
     def _read_hostname(self, filename, default=None):
-        hostname = None
         try:
-            hostname = self.readrcconf('hostname')
-        except IOError:
+            (_exists, contents) = rhel_util.read_sysconfig_file('/etc/rc.conf')
+            return contents['hostname']
+        except KeyError:
             pass
-        if not hostname:
+        else:
             return default
-        return hostname
 
     def _write_hostname(self, hostname, filename):
-        self.updatercconf('hostname', hostname)
+        rhel_util.update_sysconfig_file('/etc/rc.conf', {'hostname': hostname})
 
     def create_group(self, name, members):
         group_add_cmd = ['pw', '-n', name]
@@ -274,309 +176,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 +205,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()
@@ -650,4 +242,12 @@ class Distro(distros.Distro):
         self._runner.run("update-sources", self.package_command,
                          ["update"], freq=PER_INSTANCE)
 
+    def user_passwords(self, entries, format):
+        for i in entries:
+            user, password = i
+            if format == 'clear':
+                util.subp(['pw', 'mod', 'user', user, '-h', '0'], password)
+            else:
+                util.subp(['chpass', '-p', password, user])
+
 # vi: ts=4 expandtab
diff --git a/cloudinit/distros/netbsd.py b/cloudinit/distros/netbsd.py
new file mode 100644
index 0000000..8913018
--- /dev/null
+++ b/cloudinit/distros/netbsd.py
@@ -0,0 +1,230 @@
+# Copyright (C) 2014 Harm Weites
+# Copyright (C) 2019 Gonéri Le Bouder
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import crypt
+import os
+import six
+from six import StringIO
+
+import re
+
+from cloudinit import distros
+from cloudinit import helpers
+from cloudinit import log as logging
+from cloudinit import ssh_util
+from cloudinit import util
+from cloudinit.distros import netbsd_util
+from cloudinit.settings import PER_INSTANCE
+
+from cloudinit.distros.parsers.sys_conf import SysConf
+
+LOG = logging.getLogger(__name__)
+
+
+class Distro(distros.Distro):
+    hostname_conf_fn = '/etc/rc.conf'
+    ci_sudoers_fn = '/usr/pkg/etc/sudoers.d/90-cloud-init-users'
+
+    def __init__(self, name, cfg, paths):
+        distros.Distro.__init__(self, name, cfg, paths)
+        # This will be used to restrict certain
+        # calls from repeatly happening (when they
+        # should only happen say once per instance...)
+        self._runner = helpers.Runners(paths)
+        self.osfamily = 'netbsd'
+        cfg['ssh_svcname'] = 'sshd'
+
+
+    def _select_hostname(self, hostname, fqdn):
+        if fqdn:
+            return fqdn
+        return hostname
+
+    def _select_hostname(self, hostname, fqdn):
+        return hostname
+
+    def _read_system_hostname(self):
+        sys_hostname = self._read_hostname(filename='/etc/rc.conf')
+        return ('/etc/rc.conf', sys_hostname)
+
+    def _read_hostname(self, filename, default=None):
+        return netbsd_util.get_rc_config_value('hostname')
+
+    def _write_hostname(self, hostname, filename):
+        netbsd_util.set_rc_config_value('hostname', hostname, fn='/etc/rc.conf')
+
+    def create_group(self, name, members):
+        group_add_cmd = ['pw', '-n', name]
+        if util.is_group(name):
+            LOG.warning("Skipping creation of existing group '%s'", name)
+        else:
+            try:
+                util.subp(group_add_cmd)
+                LOG.info("Created new group %s", name)
+            except Exception as e:
+                util.logexc(LOG, "Failed to create group %s", name)
+                raise e
+
+        if len(members) > 0:
+            for member in members:
+                if not util.is_user(member):
+                    LOG.warning("Unable to add group member '%s' to group '%s'"
+                                "; user does not exist.", member, name)
+                    continue
+                try:
+                    util.subp(['pw', 'usermod', '-n', name, '-G', member])
+                    LOG.info("Added user '%s' to group '%s'", member, name)
+                except Exception:
+                    util.logexc(LOG, "Failed to add user '%s' to group '%s'",
+                                member, name)
+
+    def add_user(self, name, **kwargs):
+        if util.is_user(name):
+            LOG.info("User %s already exists, skipping.", name)
+            return False
+
+        adduser_cmd = ['useradd']
+        log_adduser_cmd = ['useradd']
+
+        adduser_opts = {
+            "homedir": '-d',
+            "gecos": '-c',
+            "primary_group": '-g',
+            "groups": '-G',
+            "shell": '-s',
+            "inactive": '-E',
+        }
+        adduser_flags = {
+            "no_user_group": '--no-user-group',
+            "system": '--system',
+            "no_log_init": '--no-log-init',
+        }
+
+        for key, val in kwargs.items():
+            if (key in adduser_opts and val and
+               isinstance(val, six.string_types)):
+                adduser_cmd.extend([adduser_opts[key], val])
+
+            elif key in adduser_flags and val:
+                adduser_cmd.append(adduser_flags[key])
+                log_adduser_cmd.append(adduser_flags[key])
+
+        if not 'no_create_home' in kwargs or not 'system' in kwargs:
+            adduser_cmd += ['-m']
+            log_adduser_cmd += ['-m']
+
+        adduser_cmd += [name]
+        log_adduser_cmd += [name]
+
+        # Run the command
+        LOG.info("Adding user %s", name)
+        try:
+            util.subp(adduser_cmd, logstring=log_adduser_cmd)
+        except Exception as e:
+            util.logexc(LOG, "Failed to create user %s", name)
+            raise e
+        # Set the password if it is provided
+        # For security consideration, only hashed passwd is assumed
+        passwd_val = kwargs.get('passwd', None)
+        if passwd_val is not None:
+            self.set_passwd(name, passwd_val, hashed=True)
+
+    def set_passwd(self, user, passwd, hashed=False):
+        if hashed:
+            hash_opt = "-H"
+        else:
+            hash_opt = "-h"
+
+        try:
+            util.subp(['pw', 'usermod', user, hash_opt, '0'],
+                      data=passwd, logstring="chpasswd for %s" % user)
+        except Exception as e:
+            util.logexc(LOG, "Failed to set password for %s", user)
+            raise e
+
+    def lock_passwd(self, name):
+        try:
+            util.subp(['usermod', '-C', 'yes', name])
+        except Exception as e:
+            util.logexc(LOG, "Failed to lock user %s", name)
+            raise e
+
+    def create_user(self, name, **kwargs):
+        self.add_user(name, **kwargs)
+
+        # Set password if plain-text password provided and non-empty
+        if 'plain_text_passwd' in kwargs and kwargs['plain_text_passwd']:
+            self.set_passwd(name, kwargs['plain_text_passwd'])
+
+        # Default locking down the account. 'lock_passwd' defaults to True.
+        # lock account unless lock_password is False.
+        if kwargs.get('lock_passwd', True):
+            self.lock_passwd(name)
+
+        # Configure sudo access
+        if 'sudo' in kwargs and kwargs['sudo'] is not False:
+            self.write_sudo_rules(name, kwargs['sudo'])
+
+        # Import SSH keys
+        if 'ssh_authorized_keys' in kwargs:
+            keys = set(kwargs['ssh_authorized_keys']) or []
+            ssh_util.setup_user_keys(keys, name, options=None)
+
+    def _write_network_config(self, netconfig):
+        return self._supported_write_network_config(netconfig)
+
+    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()
+        self.package_command('install', pkgs=pkglist)
+
+    def package_command(self, command, args=None, pkgs=None):
+        if pkgs is None:
+            pkgs = []
+
+        os_release, _ = util.subp(['uname', '-r'])
+        os_arch, _ = util.subp(['uname', '-m'])
+        e = os.environ.copy()
+        e['PKG_PATH'] = 'http://cdn.netbsd.org/pub/pkgsrc/packages/NetBSD/%s/%s/All/' % (os_arch, os_release)
+
+        if command == 'install':
+            cmd = ['pkg_add']
+        elif command == 'remove':
+            cmd = ['pkg_delete']
+        if args and isinstance(args, str):
+            cmd.append(args)
+        elif args and isinstance(args, list):
+            cmd.extend(args)
+
+        pkglist = util.expand_package_list('%s-%s', pkgs)
+        cmd.extend(pkglist)
+
+        # Allow the output of this to flow outwards (ie not be captured)
+        util.subp(cmd, env=e, capture=False)
+
+
+    def apply_locale(self, locale, out_fn=None):
+        pass
+
+    def set_timezone(self, tz):
+        distros.set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz))
+
+    def update_package_sources(self):
+        pass
+
+    def user_passwords(self, entries, format):
+        for i in entries:
+            user, password = i
+            if format == 'clear':
+                hashed_pw = crypt.crypt(password, crypt.mksalt(crypt.METHOD_BLOWFISH))
+            else:
+                hashed_pw = password
+            util.subp(['usermod', '-C', 'no', '-p', hashed_pw, user])
+
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/distros/netbsd_util.py b/cloudinit/distros/netbsd_util.py
new file mode 100644
index 0000000..a6c5e97
--- /dev/null
+++ b/cloudinit/distros/netbsd_util.py
@@ -0,0 +1,35 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+
+from cloudinit import util
+
+
+def get_rc_config_value(key, fn='/etc/rc.conf'):
+    contents = {}
+    for line in util.load_file(fn).splitlines():
+        if '=' in line:
+            k, v = line.split('=', 1)
+            contents[k] = v
+    return contents.get(key)
+
+def set_rc_config_value(key, value, fn='/etc/rc.conf'):
+    lines = []
+    done = False
+    if ' ' in value:
+        value = '"%s"' % value
+    for line in util.load_file(fn).splitlines():
+        if '=' in line:
+            k, v = line.split('=', 1)
+            if k == key:
+                v = value
+                done = True
+            lines.append('='.join([k, v]))
+        else:
+            lines.append(line)
+    if not done:
+        lines.append('='.join([key, value]))
+    with open(fn, 'w') as fd:
+        fd.write('\n'.join(lines) + '\n')
+
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/net/freebsd.py b/cloudinit/net/freebsd.py
new file mode 100644
index 0000000..bcf7db4
--- /dev/null
+++ b/cloudinit/net/freebsd.py
@@ -0,0 +1,128 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import os
+import re
+
+from cloudinit import log as logging
+from cloudinit import util
+from cloudinit.distros import rhel_util
+from cloudinit.distros.parsers.resolv_conf import ResolvConf
+
+from . import renderer
+
+LOG = logging.getLogger(__name__)
+
+
+class Renderer(renderer.Renderer):
+    resolv_conf_fn = '/etc/resolv.conf'
+
+    def __init__(self, config=None):
+        if not config:
+            config = {}
+
+    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:
+                    rhel_util.update_sysconfig_file(
+                        '/etc/rc.conf', {
+                            '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.
+                rhel_util.update_sysconfig_file(
+                    '/etc/rc.conf', {'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'
+
+            rhel_util.update_sysconfig_file(
+                '/etc/rc.conf', {'ifconfig_' + device_name: ifconfig})
+        # Note: We don't try to be clever because 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/netbsd.py b/cloudinit/net/netbsd.py
new file mode 100644
index 0000000..0a4c6fb
--- /dev/null
+++ b/cloudinit/net/netbsd.py
@@ -0,0 +1,120 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import os
+import re
+
+from cloudinit import log as logging
+from cloudinit import util
+from cloudinit.distros import rhel_util
+from cloudinit.distros import netbsd_util
+from cloudinit.distros.parsers.resolv_conf import ResolvConf
+
+from . import renderer
+
+LOG = logging.getLogger(__name__)
+
+
+class Renderer(renderer.Renderer):
+    resolv_conf_fn = '/etc/resolv.conf'
+
+    def __init__(self, config=None):
+        if not config:
+            config = {}
+
+    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'address:\s([\da-f:]{17})', blocks.pop())
+            if m and m.group(1) == mac:
+                return ifname
+
+    def _write_network(self, settings):
+        nameservers = []
+        searchdomains = []
+        dhcp_interfaces = []
+        for interface in settings.iter_interfaces():
+            device_mac = interface.get("mac_address")
+            if device_mac:
+                device_name = self._get_ifname_by_mac(device_mac)
+            if not device_name:
+                device_name = interface.get("name")
+
+            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.
+                if subnet.get('gateway'):
+                    netbsd_util.set_rc_config_value(
+                        'defaultroute', subnet.get('gateway'))
+
+                if 'dns_nameservers' in subnet:
+                    nameservers.extend(subnet['dns_nameservers'])
+                if 'dns_search' in subnet:
+                    searchdomains.extend(subnet['dns_search'])
+                netbsd_util.set_rc_config_value('ifconfig_' + device_name, ifconfig)
+            else:
+                dhcp_interfaces.append(device_name)
+
+
+        if dhcp_interfaces:
+            netbsd_util.set_rc_config_value('dhcpcd', 'YES')
+            netbsd_util.set_rc_config_value('dhcpcd_flags', ' '.join(dhcp_interfaces))
+        # Ensure the network service reload /etc/rc.conf to get a fresh
+        # copy in memory.
+        with open('/etc/rc.conf.d/network', 'w') as fd:
+            fd.write('. /etc/rc.conf\nrm /etc/rc.conf.d/network')
+        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..e4bcae9 100644
--- a/cloudinit/net/renderers.py
+++ b/cloudinit/net/renderers.py
@@ -1,17 +1,21 @@
 # This file is part of cloud-init. See LICENSE file for license information.
 
 from . import eni
+from . import freebsd
+from . import netbsd
 from . import netplan
 from . import RendererNotFoundError
 from . import sysconfig
 
 NAME_TO_RENDERER = {
     "eni": eni,
+    "freebsd": freebsd,
+    "netbsd": netbsd,
     "netplan": netplan,
     "sysconfig": sysconfig,
 }
 
-DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan"]
+DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan", "freebsd", "netbsd"]
 
 
 def search(priority=None, target=None, first=False):
diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
index e91cd26..2e4edbc 100644
--- a/cloudinit/netinfo.py
+++ b/cloudinit/netinfo.py
@@ -91,6 +91,53 @@ def _netdev_info_iproute(ipaddr_out):
     return devs
 
 
+def _netdev_info_ifconfig_netbsd(ifconfig_data):
+    # fields that need to be returned in devs for each dev
+    devs = {}
+    for line in ifconfig_data.splitlines():
+        if len(line) == 0:
+            continue
+        if line[0] not in ("\t", " "):
+            curdev = line.split()[0]
+            # current ifconfig pops a ':' on the end of the device
+            if curdev.endswith(':'):
+                curdev = curdev[:-1]
+            if curdev not in devs:
+                devs[curdev] = deepcopy(DEFAULT_NETDEV_INFO)
+        toks = line.lower().strip().split()
+        if len(toks) > 1:
+            if re.search(r"flags=[x\d]+<up.*>", toks[1]):
+                devs[curdev]['up'] = True
+
+        for i in range(len(toks)):
+            if toks[i] == "inet":  # Create new ipv4 addr entry
+                network, net_bits = toks[i + 1].split('/')
+                devs[curdev]['ipv4'].append(
+                    {'ip': network, 'mask': net_prefix_to_ipv4_mask(net_bits)})
+            elif toks[i] == "broadcast":
+                devs[curdev]['ipv4'][-1]['bcast'] = toks[i + 1]
+            elif toks[i] == "address:":
+                devs[curdev]['hwaddr'] = toks[i + 1]
+            elif toks[i] == "inet6":
+                if toks[i + 1] == "addr:":
+                    devs[curdev]['ipv6'].append({'ip': toks[i + 2]})
+                else:
+                    devs[curdev]['ipv6'].append({'ip': toks[i + 1]})
+            elif toks[i] == "prefixlen":  # Add prefix to current ipv6 value
+                addr6 = devs[curdev]['ipv6'][-1]['ip'] + "/" + toks[i + 1]
+                devs[curdev]['ipv6'][-1]['ip'] = addr6
+            elif toks[i].startswith("scope:"):
+                devs[curdev]['ipv6'][-1]['scope6'] = toks[i].lstrip("scope:")
+            elif toks[i] == "scopeid":
+                res = re.match(r'.*<(\S+)>', toks[i + 1])
+                if res:
+                    devs[curdev]['ipv6'][-1]['scope6'] = res.group(1)
+                else:
+                    devs[curdev]['ipv6'][-1]['scope6'] = toks[i + 1]
+
+    return devs
+
+
 def _netdev_info_ifconfig(ifconfig_data):
     # fields that need to be returned in devs for each dev
     devs = {}
@@ -149,7 +196,10 @@ def _netdev_info_ifconfig(ifconfig_data):
 
 def netdev_info(empty=""):
     devs = {}
-    if util.which('ip'):
+    if util.is_NetBSD():
+        (ifcfg_out, _err) = util.subp(["ifconfig", "-a"], rcs=[0, 1])
+        devs = _netdev_info_ifconfig_netbsd(ifcfg_out)
+    elif util.which('ip'):
         # Try iproute first of all
         (ipaddr_out, _err) = util.subp(["ip", "addr", "show"])
         devs = _netdev_info_iproute(ipaddr_out)
diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py
index 8a9e5dd..c79b803 100644
--- a/cloudinit/sources/DataSourceNoCloud.py
+++ b/cloudinit/sources/DataSourceNoCloud.py
@@ -40,6 +40,14 @@ class DataSourceNoCloud(sources.DataSource):
             devlist = [
                 p for p in ['/dev/msdosfs/' + label, '/dev/iso9660/' + label]
                 if os.path.exists(p)]
+        elif util.is_NetBSD():
+            out, _err = util.subp(['sysctl', '-n', 'hw.disknames'], rcs=[0])
+            devlist = []
+            for dev in out.split():
+                mscdlabel_out, _ = util.subp(['mscdlabel', dev], rcs=[0])
+                if ('label "%s"' % label) in mscdlabel_out:
+                    print(mscdlabel_out)
+                    devlist.append('/dev/' + dev)
         else:
             # Query optical drive to get it in blkid cache for 2.6 kernels
             util.find_devs_with(path="/dev/sr0")
diff --git a/cloudinit/util.py b/cloudinit/util.py
index aa23b3f..efb1db8 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -551,6 +551,10 @@ def is_FreeBSD():
     return system_info()['variant'] == "freebsd"
 
 
+def is_NetBSD():
+    return system_info()['variant'] == "netbsd"
+
+
 def get_cfg_option_bool(yobj, key, default=False):
     if key not in yobj:
         return default
@@ -667,7 +671,7 @@ def system_info():
             var = 'suse'
         else:
             var = 'linux'
-    elif system in ('windows', 'darwin', "freebsd"):
+    elif system in ('windows', 'darwin', "freebsd", "netbsd"):
         var = system
 
     info['variant'] = var
@@ -2378,6 +2382,7 @@ def get_mount_info_freebsd(path):
     return "/dev/" + label_part, ret[2], ret[1]
 
 
+
 def get_device_info_from_zpool(zpool):
     # zpool has 10 second timeout waiting for /dev/zfs LP: #1760173
     if not os.path.exists('/dev/zfs'):
diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl
index 684c747..3f80edb 100644
--- a/config/cloud.cfg.tmpl
+++ b/config/cloud.cfg.tmpl
@@ -2,7 +2,7 @@
 # The top level settings are used as module
 # and system configuration.
 
-{% if variant in ["freebsd"] %}
+{% if variant in ["freebsd", "netbsd"] %}
 syslog_fix_perms: root:wheel
 {% elif variant in ["suse"] %}
 syslog_fix_perms: root:root
@@ -48,15 +48,17 @@ cloud_init_modules:
  - seed_random
  - bootcmd
  - write-files
+{% if variant not in ["netbsd"] %}
  - growpart
  - resizefs
-{% if variant not in ["freebsd"] %}
+{% endif %}
+{% if variant not in ["freebsd", "netbsd"] %}
  - disk_setup
  - mounts
 {% endif %}
  - set_hostname
  - update_hostname
-{% if variant not in ["freebsd"] %}
+{% if variant not in ["freebsd", "netbsd"] %}
  - update_etc_hosts
  - ca-certs
  - rsyslog
@@ -91,7 +93,7 @@ cloud_config_modules:
 {% if variant in ["suse"] %}
  - zypper-add-repo
 {% endif %}
-{% if variant not in ["freebsd"] %}
+{% if variant not in ["freebsd", "netbsd"] %}
  - ntp
 {% endif %}
  - timezone
@@ -115,7 +117,7 @@ cloud_final_modules:
 {% if variant in ["ubuntu", "unknown"] %}
  - ubuntu-drivers
 {% endif %}
-{% if variant not in ["freebsd"] %}
+{% if variant not in ["freebsd", "netbsd"] %}
  - puppet
  - chef
  - mcollective
@@ -137,7 +139,7 @@ cloud_final_modules:
 # (not accessible to handlers/transforms)
 system_info:
    # This will affect which distro class gets used
-{% if variant in ["centos", "debian", "fedora", "rhel", "suse", "ubuntu", "freebsd"] %}
+{% if variant in ["centos", "debian", "fedora", "rhel", "suse", "ubuntu", "freebsd", "netbsd"] %}
    distro: {{ variant }}
 {% else %}
    # Unknown/fallback distro.
@@ -212,4 +214,19 @@ system_info:
      groups: [wheel]
      sudo: ["ALL=(ALL) NOPASSWD:ALL"]
      shell: /bin/tcsh
+{% elif variant in ["netbsd"] %}
+   default_user:
+     name: netbsd
+     lock_passwd: True
+     gecos: NetBSD
+     groups: [wheel]
+     sudo: ["ALL=(ALL) NOPASSWD:ALL"]
+     shell: /bin/sh
 {% endif %}
+{% if variant in ["freebsd"] %}
+   network:
+      renderers: ['freebsd']
+{% elif variant in ["netbsd"] %}
+   network:
+      renderers: ['netbsd']
+{% endif %}
\ No newline at end of file
diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst
index 1e99455..c0c3c1d 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', 'netbsd']
 
 
 Network Configuration Tools
diff --git a/setup.py b/setup.py
index fcaf26f..a0b6ffe 100755
--- a/setup.py
+++ b/setup.py
@@ -136,6 +136,7 @@ if '--distro' in sys.argv:
 INITSYS_FILES = {
     'sysvinit': [f for f in glob('sysvinit/redhat/*') if is_f(f)],
     'sysvinit_freebsd': [f for f in glob('sysvinit/freebsd/*') if is_f(f)],
+    'sysvinit_netbsd': [f for f in glob('sysvinit/netbsd/*') if is_f(f)],
     'sysvinit_deb': [f for f in glob('sysvinit/debian/*') if is_f(f)],
     'sysvinit_openrc': [f for f in glob('sysvinit/gentoo/*') if is_f(f)],
     'sysvinit_suse': [f for f in glob('sysvinit/suse/*') if is_f(f)],
@@ -152,6 +153,7 @@ INITSYS_FILES = {
 INITSYS_ROOTS = {
     'sysvinit': 'etc/rc.d/init.d',
     'sysvinit_freebsd': 'usr/local/etc/rc.d',
+    'sysvinit_netbsd': 'usr/local/etc/rc.d',
     'sysvinit_deb': 'etc/init.d',
     'sysvinit_openrc': 'etc/init.d',
     'sysvinit_suse': 'etc/init.d',
@@ -259,7 +261,7 @@ data_files = [
     (USR + '/share/doc/cloud-init/examples/seed',
         [f for f in glob('doc/examples/seed/*') if is_f(f)]),
 ]
-if os.uname()[0] != 'FreeBSD':
+if os.uname()[0] not in ['FreeBSD', 'NetBSD']:
     data_files.extend([
         (ETC + '/NetworkManager/dispatcher.d/',
          ['tools/hook-network-manager']),
diff --git a/sysvinit/netbsd/cloudconfig b/sysvinit/netbsd/cloudconfig
new file mode 100755
index 0000000..bf2e9a8
--- /dev/null
+++ b/sysvinit/netbsd/cloudconfig
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# PROVIDE: cloudconfig
+# REQUIRE: cloudinit cloudinitlocal
+# REQUIRE: cloudfinal
+
+$_rc_subr_loaded . /etc/rc.subr
+
+name="cloudinit"
+start_cmd="start_cloud_init"
+start_cloud_init()
+{
+    /usr/pkg/bin/cloud-init modules --mode config
+}
+
+load_rc_config $name
+run_rc_command "$1"
\ No newline at end of file
diff --git a/sysvinit/netbsd/cloudfinal b/sysvinit/netbsd/cloudfinal
new file mode 100755
index 0000000..36a15ce
--- /dev/null
+++ b/sysvinit/netbsd/cloudfinal
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+# PROVIDE: cloudfinal
+# REQUIRE: LOGIN
+
+$_rc_subr_loaded . /etc/rc.subr
+
+name="cloudinit"
+start_cmd="start_cloud_init"
+start_cloud_init()
+{
+    /usr/pkg/bin/cloud-init modules --mode final
+}
+
+load_rc_config $name
+run_rc_command "$1"
\ No newline at end of file
diff --git a/sysvinit/netbsd/cloudinit b/sysvinit/netbsd/cloudinit
new file mode 100755
index 0000000..d75109f
--- /dev/null
+++ b/sysvinit/netbsd/cloudinit
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+# PROVIDE: cloudinit
+# REQUIRE: NETWORKING
+
+$_rc_subr_loaded . /etc/rc.subr
+
+name="cloudinit"
+start_cmd="start_cloud_init"
+start_cloud_init()
+{
+    /usr/pkg/bin/cloud-init init
+}
+
+load_rc_config $name
+run_rc_command "$1"
\ No newline at end of file
diff --git a/sysvinit/netbsd/cloudinitlocal b/sysvinit/netbsd/cloudinitlocal
new file mode 100755
index 0000000..bd97f3f
--- /dev/null
+++ b/sysvinit/netbsd/cloudinitlocal
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+# PROVIDER: cloudinitlocal
+# BEFORE: NETWORKING
+
+$_rc_subr_loaded . /etc/rc.subr
+
+name="cloudinitlocal"
+start_cmd="start_cloud_init_local"
+start_cloud_init_local()
+{
+    /usr/pkg/bin/cloud-init init -l
+}
+
+load_rc_config $name
+run_rc_command "$1"
\ No newline at end of file
diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
index c3c0c8c..8367f8b 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
@@ -14,7 +15,7 @@ from cloudinit.distros.parsers.sys_conf import SysConf
 from cloudinit import helpers
 from cloudinit import settings
 from cloudinit.tests.helpers import (
-    FilesystemMockingTestCase, dir2dict, populate_dir)
+    FilesystemMockingTestCase, dir2dict)
 from cloudinit import util
 
 
@@ -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',
-        }
+    @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
+"""
 
-        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',
+        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())
+
+    @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
+"""
 
-        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))
+        V1_NET_CFG_DNS = copy.deepcopy(V1_NET_CFG)
+        ns = ['1.2.3.4']
+        V1_NET_CFG_DNS['config'][0]['subnets'][0]['dns_nameservers'] = ns
+        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):
diff --git a/tools/build-on-freebsd b/tools/build-on-freebsd
index dc3b974..5c62bd4 100755
--- a/tools/build-on-freebsd
+++ b/tools/build-on-freebsd
@@ -9,7 +9,6 @@ fail() { echo "FAILED:" "$@" 1>&2; exit 1; }
 depschecked=/tmp/c-i.dependencieschecked
 pkgs="
    bash
-   chpasswd
    dmidecode
    e2fsprogs
    py27-Jinja2
diff --git a/tools/build-on-netbsd b/tools/build-on-netbsd
new file mode 100755
index 0000000..97b6cf0
--- /dev/null
+++ b/tools/build-on-netbsd
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+fail() { echo "FAILED:" "$@" 1>&2; exit 1; }
+
+# Check dependencies:
+depschecked=/tmp/c-i.dependencieschecked
+pkgs="
+   bash
+   dmidecode
+   py37-configobj
+   py37-jinja2
+   py37-oauthlib
+   py37-requests
+   py37-setuptools
+   py37-six
+   py37-yaml
+   sudo
+"
+[ -f "$depschecked" ] || pkg_add ${pkgs} || fail "install packages"
+
+pkg_add py37-pip
+pip3.7 --no-cache-dir install jsonpatch
+pip3.7 --no-cache-dir install jsonschema
+touch $depschecked
+
+# Build the code and install in /usr/pkg/:
+python3.7 setup.py build
+python3.7 setup.py install -O1 --distro netbsd --skip-build --init-system sysvinit_netbsd
+mv -v /usr/local/etc/rc.d/cloud* /etc/rc.d
+
+# Enable cloud-init in /etc/rc.conf:
+sed -i.bak -e "/^cloud.*=.*/d" /etc/rc.conf
+echo '
+# You can safely remove the following lines starting with "cloud"
+cloudinitlocal="YES"
+cloudinit="YES"
+cloudconfig="YES"
+cloudinitlocal="YES"' >> /etc/rc.conf
+
+echo "Installation completed."
diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg
index 0957c32..8be8134 100755
--- a/tools/render-cloudcfg
+++ b/tools/render-cloudcfg
@@ -4,7 +4,7 @@ import argparse
 import os
 import sys
 
-VARIANTS = ["freebsd", "centos", "fedora", "rhel", "suse", "ubuntu", "unknown"]
+VARIANTS = ["freebsd", "centos", "fedora", "rhel", "suse", "ubuntu", "unknown", "netbsd"]
 
 if "avoid-pep8-E402-import-not-top-of-file":
     _tdir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))

Follow ups