summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/distros/bsd.py111
-rw-r--r--cloudinit/distros/bsd_utils.py50
-rw-r--r--cloudinit/distros/freebsd.py133
-rw-r--r--cloudinit/distros/netbsd.py133
-rw-r--r--cloudinit/net/__init__.py27
-rw-r--r--cloudinit/net/bsd.py165
-rw-r--r--cloudinit/net/freebsd.py169
-rw-r--r--cloudinit/net/netbsd.py42
-rw-r--r--cloudinit/net/renderers.py4
-rw-r--r--cloudinit/netinfo.py52
-rw-r--r--cloudinit/sources/DataSourceConfigDrive.py8
-rw-r--r--cloudinit/sources/DataSourceNoCloud.py8
-rw-r--r--cloudinit/tests/helpers.py1
-rw-r--r--cloudinit/tests/test_util.py34
-rw-r--r--cloudinit/util.py33
-rw-r--r--config/cloud.cfg.tmpl30
-rw-r--r--doc/rtd/topics/network-config.rst2
-rwxr-xr-xsetup.py4
-rwxr-xr-xsysvinit/netbsd/cloudconfig17
-rwxr-xr-xsysvinit/netbsd/cloudfinal16
-rwxr-xr-xsysvinit/netbsd/cloudinit16
-rwxr-xr-xsysvinit/netbsd/cloudinitlocal18
-rw-r--r--tests/unittests/test_distros/test_bsd_utils.py66
-rwxr-xr-xtools/build-on-netbsd37
-rwxr-xr-xtools/render-cloudcfg4
25 files changed, 884 insertions, 296 deletions
diff --git a/cloudinit/distros/bsd.py b/cloudinit/distros/bsd.py
new file mode 100644
index 00000000..e9b84edc
--- /dev/null
+++ b/cloudinit/distros/bsd.py
@@ -0,0 +1,111 @@
+import platform
+
+from cloudinit import distros
+from cloudinit.distros import bsd_utils
+from cloudinit import helpers
+from cloudinit import log as logging
+from cloudinit import net
+from cloudinit import util
+
+LOG = logging.getLogger(__name__)
+
+
+class BSD(distros.Distro):
+ hostname_conf_fn = '/etc/rc.conf'
+ rc_conf_fn = "/etc/rc.conf"
+
+ # Set in BSD distro subclasses
+ group_add_cmd_prefix = []
+ pkg_cmd_install_prefix = []
+ pkg_cmd_remove_prefix = []
+
+ def __init__(self, name, cfg, paths):
+ super().__init__(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)
+ cfg['ssh_svcname'] = 'sshd'
+ self.osfamily = platform.system().lower()
+
+ def _read_system_hostname(self):
+ sys_hostname = self._read_hostname(self.hostname_conf_fn)
+ return (self.hostname_conf_fn, sys_hostname)
+
+ def _read_hostname(self, filename, default=None):
+ return bsd_utils.get_rc_config_value('hostname')
+
+ def _get_add_member_to_group_cmd(self, member_name, group_name):
+ raise NotImplementedError('Return list cmd to add member to group')
+
+ def _write_hostname(self, hostname, filename):
+ bsd_utils.set_rc_config_value('hostname', hostname, fn='/etc/rc.conf')
+
+ def create_group(self, name, members=None):
+ if util.is_group(name):
+ LOG.warning("Skipping creation of existing group '%s'", name)
+ else:
+ group_add_cmd = self.group_add_cmd_prefix + [name]
+ try:
+ util.subp(group_add_cmd)
+ LOG.info("Created new group %s", name)
+ except Exception:
+ util.logexc(LOG, "Failed to create group %s", name)
+
+ if not members:
+ members = []
+ 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(self._get_add_member_to_group_cmd(member, name))
+ 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 generate_fallback_config(self):
+ nconf = {'config': [], 'version': 1}
+ for mac, name in net.get_interfaces_by_mac().items():
+ nconf['config'].append(
+ {'type': 'physical', 'name': name,
+ 'mac_address': mac, 'subnets': [{'type': 'dhcp'}]})
+ return nconf
+
+ def install_packages(self, pkglist):
+ self.update_package_sources()
+ self.package_command('install', pkgs=pkglist)
+
+ def _get_pkg_cmd_environ(self):
+ """Return environment vars used in *BSD package_command operations"""
+ raise NotImplementedError('BSD subclasses return a dict of env vars')
+
+ def package_command(self, command, args=None, pkgs=None):
+ if pkgs is None:
+ pkgs = []
+
+ # TODO neither freebsd nor netbsd handles a command 'upgrade'
+ # provided by cloudinit/config/cc_package_update_upgrade_install.py
+ if command == 'install':
+ cmd = self.pkg_cmd_install_prefix
+ elif command == 'remove':
+ cmd = self.pkg_cmd_remove_prefix
+
+ 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=self._get_pkg_cmd_environ(), capture=False)
+
+ def _write_network_config(self, netconfig):
+ return self._supported_write_network_config(netconfig)
+
+ def set_timezone(self, tz):
+ distros.set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz))
diff --git a/cloudinit/distros/bsd_utils.py b/cloudinit/distros/bsd_utils.py
new file mode 100644
index 00000000..079d0d53
--- /dev/null
+++ b/cloudinit/distros/bsd_utils.py
@@ -0,0 +1,50 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import shlex
+
+from cloudinit import util
+
+# On NetBSD, /etc/rc.conf comes with a if block:
+# if [ -r /etc/defaults/rc.conf ]; then
+# as a consequence, the file is not a regular key/value list
+# anymore and we cannot use cloudinit.distros.parsers.sys_conf
+# The module comes with a more naive parser, but is able to
+# preserve these if blocks.
+
+
+def _unquote(value):
+ if value[0] == value[-1] and value[0] in ['"', "'"]:
+ return value[1:-1]
+ return value
+
+
+def get_rc_config_value(key, fn='/etc/rc.conf'):
+ key_prefix = '{}='.format(key)
+ for line in util.load_file(fn).splitlines():
+ if line.startswith(key_prefix):
+ value = line.replace(key_prefix, '')
+ return _unquote(value)
+
+
+def set_rc_config_value(key, value, fn='/etc/rc.conf'):
+ lines = []
+ done = False
+ value = shlex.quote(value)
+ original_content = util.load_file(fn)
+ for line in original_content.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]))
+ new_content = '\n'.join(lines) + '\n'
+ if new_content != original_content:
+ util.write_file(fn, new_content)
+
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py
index 026d1142..a775ae51 100644
--- a/cloudinit/distros/freebsd.py
+++ b/cloudinit/distros/freebsd.py
@@ -8,34 +8,22 @@ import os
import re
from io import StringIO
-from cloudinit import distros
-from cloudinit import helpers
+import cloudinit.distros.bsd
from cloudinit import log as logging
-from cloudinit import net
-from cloudinit import ssh_util
from cloudinit import util
-from cloudinit.distros import rhel_util
from cloudinit.settings import PER_INSTANCE
LOG = logging.getLogger(__name__)
-class Distro(distros.Distro):
+class Distro(cloudinit.distros.bsd.BSD):
usr_lib_exec = '/usr/local/lib'
- rc_conf_fn = "/etc/rc.conf"
login_conf_fn = '/etc/login.conf'
login_conf_fn_bak = '/etc/login.conf.orig'
ci_sudoers_fn = '/usr/local/etc/sudoers.d/90-cloud-init-users'
- hostname_conf_fn = '/etc/rc.conf'
-
- 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 = 'freebsd'
- cfg['ssh_svcname'] = 'sshd'
+ group_add_cmd_prefix = ['pw', 'group', 'add']
+ pkg_cmd_install_prefix = ["pkg", "install"]
+ pkg_cmd_remove_prefix = ["pkg", "remove"]
def _select_hostname(self, hostname, fqdn):
# Should be FQDN if available. See rc.conf(5) in FreeBSD
@@ -43,45 +31,8 @@ class Distro(distros.Distro):
return fqdn
return hostname
- def _read_system_hostname(self):
- sys_hostname = self._read_hostname(self.hostname_conf_fn)
- return (self.hostname_conf_fn, sys_hostname)
-
- def _read_hostname(self, filename, default=None):
- (_exists, contents) = rhel_util.read_sysconfig_file(filename)
- if contents.get('hostname'):
- return contents['hostname']
- else:
- return default
-
- def _write_hostname(self, hostname, filename):
- rhel_util.update_sysconfig_file(filename, {'hostname': hostname})
-
- def create_group(self, name, members):
- group_add_cmd = ['pw', 'group', 'add', 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:
- util.logexc(LOG, "Failed to create group %s", name)
- raise
- if not members:
- members = []
-
- 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 _get_add_member_to_group_cmd(self, member_name, group_name):
+ return ['pw', 'usermod', '-n', member_name, '-G', group_name]
def add_user(self, name, **kwargs):
if util.is_user(name):
@@ -162,40 +113,8 @@ class Distro(distros.Distro):
util.logexc(LOG, "Failed to lock user %s", name)
raise
- 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 generate_fallback_config(self):
- nconf = {'config': [], 'version': 1}
- for mac, name in net.get_interfaces_by_mac().items():
- nconf['config'].append(
- {'type': 'physical', 'name': name,
- 'mac_address': mac, 'subnets': [{'type': 'dhcp'}]})
- return nconf
-
- 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
+ # Adjust the locales value to the new value
newconf = StringIO()
for line in util.load_file(self.login_conf_fn).splitlines():
newconf.write(re.sub(r'^default:',
@@ -225,39 +144,17 @@ class Distro(distros.Distro):
# /etc/rc.conf a line with the following format:
# ifconfig_OLDNAME_name=NEWNAME
# FreeBSD network script will rename the interface automatically.
- 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 = []
+ pass
+ def _get_pkg_cmd_environ(self):
+ """Return environment vars used in *BSD package_command operations"""
e = os.environ.copy()
e['ASSUME_ALWAYS_YES'] = 'YES'
-
- cmd = ['pkg']
- if args and isinstance(args, str):
- cmd.append(args)
- elif args and isinstance(args, list):
- cmd.extend(args)
-
- if command:
- cmd.append(command)
-
- 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 set_timezone(self, tz):
- distros.set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz))
+ return e
def update_package_sources(self):
- self._runner.run("update-sources", self.package_command,
- ["update"], freq=PER_INSTANCE)
+ self._runner.run(
+ "update-sources", self.package_command,
+ ["update"], freq=PER_INSTANCE)
# vi: ts=4 expandtab
diff --git a/cloudinit/distros/netbsd.py b/cloudinit/distros/netbsd.py
new file mode 100644
index 00000000..353eb671
--- /dev/null
+++ b/cloudinit/distros/netbsd.py
@@ -0,0 +1,133 @@
+# Copyright (C) 2019-2020 Gonéri Le Bouder
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import crypt
+import os
+import platform
+import six
+
+import cloudinit.distros.bsd
+from cloudinit import log as logging
+from cloudinit import util
+
+LOG = logging.getLogger(__name__)
+
+
+class Distro(cloudinit.distros.bsd.BSD):
+ ci_sudoers_fn = '/usr/pkg/etc/sudoers.d/90-cloud-init-users'
+
+ group_add_cmd_prefix = ["groupadd"]
+ pkg_cmd_install_prefix = ["pkg_add", "-U"]
+ pkg_cmd_remove_prefix = ['pkg_delete']
+
+ def _get_add_member_to_group_cmd(self, member_name, group_name):
+ return ['usermod', '-G', group_name, 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',
+ }
+ 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 'no_create_home' not in kwargs or 'system' not 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:
+ util.logexc(LOG, "Failed to create user %s", name)
+ raise
+ # 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:
+ hashed_pw = passwd
+ elif not hasattr(crypt, 'METHOD_BLOWFISH'):
+ # crypt.METHOD_BLOWFISH comes with Python 3.7 which is available
+ # on NetBSD 7 and 8.
+ LOG.error((
+ 'Cannot set non-encrypted password for user %s. '
+ 'Python >= 3.7 is required.'), user)
+ return
+ else:
+ method = crypt.METHOD_BLOWFISH # pylint: disable=E1101
+ hashed_pw = crypt.crypt(
+ passwd,
+ crypt.mksalt(method))
+
+ try:
+ util.subp(['usermod', '-C', 'no', '-p', hashed_pw, user])
+ except Exception:
+ util.logexc(LOG, "Failed to set password for %s", user)
+ raise
+
+ def force_passwd_change(self, user):
+ try:
+ util.subp(['usermod', '-F', user])
+ except Exception:
+ util.logexc(LOG, "Failed to set pw expiration for %s", user)
+ raise
+
+ def lock_passwd(self, name):
+ try:
+ util.subp(['usermod', '-C', 'yes', name])
+ except Exception:
+ util.logexc(LOG, "Failed to lock user %s", name)
+ raise
+
+ def apply_locale(self, locale, out_fn=None):
+ LOG.debug('Cannot set the locale.')
+
+ def apply_network_config_names(self, netconfig):
+ LOG.debug('NetBSD cannot rename network interface.')
+
+ def _get_pkg_cmd_environ(self):
+ """Return environment vars used in *BSD package_command operations"""
+ os_release = platform.release()
+ os_arch = platform.machine()
+ e = os.environ.copy()
+ e['PKG_PATH'] = (
+ 'http://cdn.netbsd.org/pub/pkgsrc/'
+ 'packages/NetBSD/%s/%s/All') % (os_arch, os_release)
+ return e
+
+ def update_package_sources(self):
+ pass
+
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index 1d5eb535..400d7870 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -334,10 +334,20 @@ def find_fallback_nic(blacklist_drivers=None):
"""Return the name of the 'fallback' network device."""
if util.is_FreeBSD():
return find_fallback_nic_on_freebsd(blacklist_drivers)
+ elif util.is_NetBSD():
+ return find_fallback_nic_on_netbsd(blacklist_drivers)
else:
return find_fallback_nic_on_linux(blacklist_drivers)
+def find_fallback_nic_on_netbsd(blacklist_drivers=None):
+ values = list(sorted(
+ get_interfaces_by_mac().values(),
+ key=natural_sort_key))
+ if values:
+ return values[0]
+
+
def find_fallback_nic_on_freebsd(blacklist_drivers=None):
"""Return the name of the 'fallback' network device on FreeBSD.
@@ -799,6 +809,8 @@ def get_ib_interface_hwaddr(ifname, ethernet_format):
def get_interfaces_by_mac():
if util.is_FreeBSD():
return get_interfaces_by_mac_on_freebsd()
+ elif util.is_NetBSD():
+ return get_interfaces_by_mac_on_netbsd()
else:
return get_interfaces_by_mac_on_linux()
@@ -830,6 +842,21 @@ def get_interfaces_by_mac_on_freebsd():
return results
+def get_interfaces_by_mac_on_netbsd():
+ ret = {}
+ re_field_match = (
+ r"(?P<ifname>\w+).*address:\s"
+ r"(?P<mac>([\da-f]{2}[:-]){5}([\da-f]{2})).*")
+ (out, _) = util.subp(['ifconfig', '-a'])
+ if_lines = re.sub(r'\n\s+', ' ', out).splitlines()
+ for line in if_lines:
+ m = re.match(re_field_match, line)
+ if m:
+ fields = m.groupdict()
+ ret[fields['mac']] = fields['ifname']
+ return ret
+
+
def get_interfaces_by_mac_on_linux():
"""Build a dictionary of tuples {mac: name}.
diff --git a/cloudinit/net/bsd.py b/cloudinit/net/bsd.py
new file mode 100644
index 00000000..fb714d4c
--- /dev/null
+++ b/cloudinit/net/bsd.py
@@ -0,0 +1,165 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import re
+
+from cloudinit import log as logging
+from cloudinit import net
+from cloudinit import util
+from cloudinit.distros.parsers.resolv_conf import ResolvConf
+from cloudinit.distros import bsd_utils
+
+from . import renderer
+
+LOG = logging.getLogger(__name__)
+
+
+class BSDRenderer(renderer.Renderer):
+ resolv_conf_fn = 'etc/resolv.conf'
+ rc_conf_fn = 'etc/rc.conf'
+
+ def get_rc_config_value(self, key):
+ fn = util.target_path(self.target, self.rc_conf_fn)
+ bsd_utils.get_rc_config_value(key, fn=fn)
+
+ def set_rc_config_value(self, key, value):
+ fn = util.target_path(self.target, self.rc_conf_fn)
+ bsd_utils.set_rc_config_value(key, value, fn=fn)
+
+ def __init__(self, config=None):
+ if not config:
+ config = {}
+ self.target = None
+ self.interface_configurations = {}
+ self._postcmds = config.get('postcmds', True)
+
+ def _ifconfig_entries(self, settings, target=None):
+ ifname_by_mac = net.get_interfaces_by_mac()
+ for interface in settings.iter_interfaces():
+ device_name = interface.get("name")
+ device_mac = interface.get("mac_address")
+ if device_name and re.match(r'^lo\d+$', device_name):
+ continue
+ if device_mac not in ifname_by_mac:
+ LOG.info('Cannot find any device with MAC %s', device_mac)
+ elif device_mac and device_name:
+ cur_name = ifname_by_mac[device_mac]
+ if cur_name != device_name:
+ LOG.info('netif service will rename interface %s to %s',
+ cur_name, device_name)
+ try:
+ self.rename_interface(cur_name, device_name)
+ except NotImplementedError:
+ LOG.error((
+ 'Interface renaming is '
+ 'not supported on this OS'))
+ device_name = cur_name
+
+ else:
+ device_name = ifname_by_mac[device_mac]
+
+ LOG.info('Configuring interface %s', device_name)
+
+ self.interface_configurations[device_name] = 'DHCP'
+
+ for subnet in interface.get("subnets", []):
+ if subnet.get('type') == 'static':
+ if not subnet.get('netmask'):
+ LOG.debug(
+ 'Skipping IP %s, because there is no netmask',
+ subnet.get('address'))
+ continue
+ LOG.debug('Configuring dev %s with %s / %s', device_name,
+ subnet.get('address'), subnet.get('netmask'))
+
+ self.interface_configurations[device_name] = {
+ 'address': subnet.get('address'),
+ 'netmask': subnet.get('netmask'),
+ }
+
+ def _route_entries(self, settings, target=None):
+ routes = list(settings.iter_routes())
+ for interface in settings.iter_interfaces():
+ subnets = interface.get("subnets", [])
+ for subnet in subnets:
+ if subnet.get('type') != 'static':
+ continue
+ gateway = subnet.get('gateway')
+ if gateway and len(gateway.split('.')) == 4:
+ routes.append({
+ 'network': '0.0.0.0',
+ 'netmask': '0.0.0.0',
+ 'gateway': gateway})
+ routes += subnet.get('routes', [])
+ for route in routes:
+ network = route.get('network')
+ if not network:
+ LOG.debug('Skipping a bad route entry')
+ continue
+ netmask = route.get('netmask')
+ gateway = route.get('gateway')
+ self.set_route(network, netmask, gateway)
+
+ def _resolve_conf(self, settings, target=None):
+ nameservers = settings.dns_nameservers
+ searchdomains = settings.dns_searchdomains
+ for interface in settings.iter_interfaces():
+ for subnet in interface.get("subnets", []):
+ if 'dns_nameservers' in subnet:
+ nameservers.extend(subnet['dns_nameservers'])
+ if 'dns_search' in subnet:
+ searchdomains.extend(subnet['dns_search'])
+ # Try to read the /etc/resolv.conf or just start from scratch if that
+ # fails.
+ try:
+ resolvconf = ResolvConf(util.load_file(util.target_path(
+ target, self.resolv_conf_fn)))
+ resolvconf.parse()
+ except IOError:
+ util.logexc(LOG, "Failed to parse %s, use new empty file",
+ util.target_path(target, 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(
+ util.target_path(target, self.resolv_conf_fn),
+ str(resolvconf), 0o644)
+
+ def render_network_state(self, network_state, templates=None, target=None):
+ self._ifconfig_entries(settings=network_state)
+ self._route_entries(settings=network_state)
+ self._resolve_conf(settings=network_state)
+
+ self.write_config()
+ self.start_services(run=self._postcmds)
+
+ def dhcp_interfaces(self):
+ ic = self.interface_configurations.items
+ return [k for k, v in ic() if v == 'DHCP']
+
+ def start_services(self, run=False):
+ raise NotImplementedError()
+
+ def write_config(self, target=None):
+ raise NotImplementedError()
+
+ def set_gateway(self, gateway):
+ raise NotImplementedError()
+
+ def rename_interface(self, cur_name, device_name):
+ raise NotImplementedError()
+
+ def set_route(self, network, netmask, gateway):
+ raise NotImplementedError()
diff --git a/cloudinit/net/freebsd.py b/cloudinit/net/freebsd.py
index d6f61da3..60f05bb2 100644
--- a/cloudinit/net/freebsd.py
+++ b/cloudinit/net/freebsd.py
@@ -1,156 +1,29 @@
# This file is part of cloud-init. See LICENSE file for license information.
-import re
-
from cloudinit import log as logging
-from cloudinit import net
+import cloudinit.net.bsd
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'
- rc_conf_fn = 'etc/rc.conf'
+class Renderer(cloudinit.net.bsd.BSDRenderer):
def __init__(self, config=None):
- if not config:
- config = {}
- self.dhcp_interfaces = []
- self._postcmds = config.get('postcmds', True)
-
- def _update_rc_conf(self, settings, target=None):
- fn = util.target_path(target, self.rc_conf_fn)
- rhel_util.update_sysconfig_file(fn, settings)
-
- def _write_ifconfig_entries(self, settings, target=None):
- ifname_by_mac = net.get_interfaces_by_mac()
- for interface in settings.iter_interfaces():
- device_name = interface.get("name")
- device_mac = interface.get("mac_address")
- if device_name and re.match(r'^lo\d+$', device_name):
- continue
- if device_mac not in ifname_by_mac:
- LOG.info('Cannot find any device with MAC %s', device_mac)
- elif device_mac and device_name:
- cur_name = ifname_by_mac[device_mac]
- if cur_name != device_name:
- LOG.info('netif service will rename interface %s to %s',
- cur_name, device_name)
- self._update_rc_conf(
- {'ifconfig_%s_name' % cur_name: device_name},
- target=target)
- else:
- device_name = ifname_by_mac[device_mac]
-
- LOG.info('Configuring interface %s', device_name)
- ifconfig = 'DHCP' # default
-
- for subnet in interface.get("subnets", []):
- if ifconfig != 'DHCP':
- LOG.info('The FreeBSD provider only set the first subnet.')
- break
- if subnet.get('type') == 'static':
- if not subnet.get('netmask'):
- LOG.debug(
- 'Skipping IP %s, because there is no netmask',
- subnet.get('address'))
- continue
- 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'))
-
- if ifconfig == 'DHCP':
- self.dhcp_interfaces.append(device_name)
- self._update_rc_conf(
- {'ifconfig_' + device_name: ifconfig},
- target=target)
-
- def _write_route_entries(self, settings, target=None):
- routes = list(settings.iter_routes())
- for interface in settings.iter_interfaces():
- subnets = interface.get("subnets", [])
- for subnet in subnets:
- if subnet.get('type') != 'static':
- continue
- gateway = subnet.get('gateway')
- if gateway and len(gateway.split('.')) == 4:
- routes.append({
- 'network': '0.0.0.0',
- 'netmask': '0.0.0.0',
- 'gateway': gateway})
- routes += subnet.get('routes', [])
- route_cpt = 0
- for route in routes:
- network = route.get('network')
- if not network:
- LOG.debug('Skipping a bad route entry')
- continue
- netmask = route.get('netmask')
- gateway = route.get('gateway')
- route_cmd = "-route %s/%s %s" % (network, netmask, gateway)
- if network == '0.0.0.0':
- self._update_rc_conf(
- {'defaultrouter': gateway}, target=target)
+ self._route_cpt = 0
+ super(Renderer, self).__init__()
+
+ def rename_interface(self, cur_name, device_name):
+ self.set_rc_config_value('ifconfig_%s_name' % cur_name, device_name)
+
+ def write_config(self):
+ for device_name, v in self.interface_configurations.items():
+ if isinstance(v, dict):
+ self.set_rc_config_value(
+ 'ifconfig_' + device_name,
+ v.get('address') + ' netmask ' + v.get('netmask'))
else:
- self._update_rc_conf(
- {'route_net%d' % route_cpt: route_cmd}, target=target)
- route_cpt += 1
-
- def _write_resolve_conf(self, settings, target=None):
- nameservers = settings.dns_nameservers
- searchdomains = settings.dns_searchdomains
- for interface in settings.iter_interfaces():
- for subnet in interface.get("subnets", []):
- if 'dns_nameservers' in subnet:
- nameservers.extend(subnet['dns_nameservers'])
- if 'dns_search' in subnet:
- searchdomains.extend(subnet['dns_search'])
- # Try to read the /etc/resolv.conf or just start from scratch if that
- # fails.
- try:
- resolvconf = ResolvConf(util.load_file(util.target_path(
- target, self.resolv_conf_fn)))
- resolvconf.parse()
- except IOError:
- util.logexc(LOG, "Failed to parse %s, use new empty file",
- util.target_path(target, 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(
- util.target_path(target, self.resolv_conf_fn),
- str(resolvconf), 0o644)
-
- def _write_network(self, settings, target=None):
- self._write_ifconfig_entries(settings, target=target)
- self._write_route_entries(settings, target=target)
- self._write_resolve_conf(settings, target=target)
-
- self.start_services(run=self._postcmds)
-
- def render_network_state(self, network_state, templates=None, target=None):
- self._write_network(network_state, target=target)
+ self.set_rc_config_value('ifconfig_' + device_name, 'DHCP')
def start_services(self, run=False):
if not run:
@@ -165,11 +38,21 @@ class Renderer(renderer.Renderer):
# - dhclient: it cannot stop the dhclient started by the netif service.
# In both case, the situation is ok, and we can proceed.
util.subp(['service', 'routing', 'restart'], capture=True, rcs=[0, 1])
- for dhcp_interface in self.dhcp_interfaces:
+
+ for dhcp_interface in self.dhcp_interfaces():
util.subp(['service', 'dhclient', 'restart', dhcp_interface],
rcs=[0, 1],
capture=True)
+ def set_route(self, network, netmask, gateway):
+ if network == '0.0.0.0':
+ self.set_rc_config_value('defaultrouter', gateway)
+ else:
+ route_name = 'route_net%d' % self._route_cpt
+ route_cmd = "-route %s/%s %s" % (network, netmask, gateway)
+ self.set_rc_config_value(route_name, route_cmd)
+ self._route_cpt += 1
+
def available(target=None):
return util.is_FreeBSD()
diff --git a/cloudinit/net/netbsd.py b/cloudinit/net/netbsd.py
new file mode 100644
index 00000000..9cc8ef31
--- /dev/null
+++ b/cloudinit/net/netbsd.py
@@ -0,0 +1,42 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from cloudinit import log as logging
+from cloudinit import util
+import cloudinit.net.bsd
+
+LOG = logging.getLogger(__name__)
+
+
+class Renderer(cloudinit.net.bsd.BSDRenderer):
+
+ def __init__(self, config=None):
+ super(Renderer, self).__init__()
+
+ def write_config(self):
+ if self.dhcp_interfaces():
+ self.set_rc_config_value('dhcpcd', 'YES')
+ self.set_rc_config_value(
+ 'dhcpcd_flags',
+ ' '.join(self.dhcp_interfaces()))
+ for device_name, v in self.interface_configurations.items():
+ if isinstance(v, dict):
+ self.set_rc_config_value(
+ 'ifconfig_' + device_name,
+ v.get('address') + ' netmask ' + v.get('netmask'))
+
+ def start_services(self, run=False):
+ if not run:
+ LOG.debug("netbsd generate postcmd disabled")
+ return
+
+ util.subp(['service', 'network', 'restart'], capture=True)
+ if self.dhcp_interfaces():
+ util.subp(['service', 'dhcpcd', 'restart'], capture=True)
+
+ def set_route(self, network, netmask, gateway):
+ if network == '0.0.0.0':
+ self.set_rc_config_value('defaultroute', gateway)
+
+
+def available(target=None):
+ return util.is_NetBSD()
diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py
index b98dbbe3..e4bcae9d 100644
--- a/cloudinit/net/renderers.py
+++ b/cloudinit/net/renderers.py
@@ -2,6 +2,7 @@
from . import eni
from . import freebsd
+from . import netbsd
from . import netplan
from . import RendererNotFoundError
from . import sysconfig
@@ -9,11 +10,12 @@ from . import sysconfig
NAME_TO_RENDERER = {
"eni": eni,
"freebsd": freebsd,
+ "netbsd": netbsd,
"netplan": netplan,
"sysconfig": sysconfig,
}
-DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan", "freebsd"]
+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 6ba21f4d..1001f149 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/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py
index f77923c2..dee8cde4 100644
--- a/cloudinit/sources/DataSourceConfigDrive.py
+++ b/cloudinit/sources/DataSourceConfigDrive.py
@@ -71,11 +71,11 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource):
if not found:
dslist = self.sys_cfg.get('datasource_list')
for dev in find_candidate_devs(dslist=dslist):
- try:
- if util.is_FreeBSD() and dev.startswith("/dev/cd"):
+ mtype = None
+ if (util.is_FreeBSD() or util.is_NetBSD()):
+ if dev.startswith("/dev/cd"):
mtype = "cd9660"
- else:
- mtype = None
+ try:
results = util.mount_cb(dev, read_config_drive,
mtype=mtype)
found = dev
diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py
index ee748b41..2a44128d 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, 1])
+ if ('label "%s"' % label) in mscdlabel_out:
+ devlist.append('/dev/' + dev)
+ devlist.append('/dev/' + dev + 'a') # NetBSD 7
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/tests/helpers.py b/cloudinit/tests/helpers.py
index 70f6bad7..8adb9e75 100644
--- a/cloudinit/tests/helpers.py
+++ b/cloudinit/tests/helpers.py
@@ -363,6 +363,7 @@ class FilesystemMockingTestCase(ResourceUsingTestCase):
root = self.tmp_dir()
self.patchUtils(root)
self.patchOS(root)
+ self.patchOpen(root)
return root
@contextmanager
diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py
index 11f37000..815da0fd 100644
--- a/cloudinit/tests/test_util.py
+++ b/cloudinit/tests/test_util.py
@@ -419,12 +419,6 @@ class TestGetLinuxDistro(CiTestCase):
if path == '/etc/redhat-release':
return 1
- @classmethod
- def freebsd_version_exists(self, path):
- """Side effect function """
- if path == '/bin/freebsd-version':
- return 1
-
@mock.patch('cloudinit.util.load_file')
def test_get_linux_distro_quoted_name(self, m_os_release, m_path_exists):
"""Verify we get the correct name if the os-release file has
@@ -443,11 +437,16 @@ class TestGetLinuxDistro(CiTestCase):
dist = util.get_linux_distro()
self.assertEqual(('ubuntu', '16.04', 'xenial'), dist)
- @mock.patch('cloudinit.util.subp')
- def test_get_linux_freebsd(self, m_subp, m_path_exists):
+ @mock.patch('platform.system')
+ @mock.patch('platform.release')
+ @mock.patch('cloudinit.util._parse_redhat_release')
+ def test_get_linux_freebsd(self, m_path_exists, m_platform_release,
+ m_platform_system, m_parse_redhat_release):
"""Verify we get the correct name and release name on FreeBSD."""
- m_path_exists.side_effect = TestGetLinuxDistro.freebsd_version_exists
- m_subp.return_value = ("12.0-RELEASE-p10\n", '')
+ m_path_exists.return_value = False
+ m_platform_release.return_value = '12.0-RELEASE-p10'
+ m_platform_system.return_value = 'FreeBSD'
+ m_parse_redhat_release.return_value = {}
dist = util.get_linux_distro()
self.assertEqual(('freebsd', '12.0-RELEASE-p10', ''), dist)
@@ -538,27 +537,36 @@ class TestGetLinuxDistro(CiTestCase):
self.assertEqual(
('opensuse-tumbleweed', '20180920', platform.machine()), dist)
+ @mock.patch('platform.system')
@mock.patch('platform.dist', create=True)
- def test_get_linux_distro_no_data(self, m_platform_dist, m_path_exists):
+ def test_get_linux_distro_no_data(self, m_platform_dist,
+ m_platform_system, m_path_exists):
"""Verify we get no information if os-release does not exist"""
m_platform_dist.return_value = ('', '', '')
+ m_platform_system.return_value = "Linux"
m_path_exists.return_value = 0
dist = util.get_linux_distro()
self.assertEqual(('', '', ''), dist)
+ @mock.patch('platform.system')
@mock.patch('platform.dist', create=True)
- def test_get_linux_distro_no_impl(self, m_platform_dist, m_path_exists):
+ def test_get_linux_distro_no_impl(self, m_platform_dist,
+ m_platform_system, m_path_exists):
"""Verify we get an empty tuple when no information exists and
Exceptions are not propagated"""
m_platform_dist.side_effect = Exception()
+ m_platform_system.return_value = "Linux"
m_path_exists.return_value = 0
dist = util.get_linux_distro()
self.assertEqual(('', '', ''), dist)
+ @mock.patch('platform.system')
@mock.patch('platform.dist', create=True)
- def test_get_linux_distro_plat_data(self, m_platform_dist, m_path_exists):
+ def test_get_linux_distro_plat_data(self, m_platform_dist,
+ m_platform_system, m_path_exists):
"""Verify we get the correct platform information"""
m_platform_dist.return_value = ('foo', '1.1', 'aarch64')
+ m_platform_system.return_value = "Linux"
m_path_exists.return_value = 0
dist = util.get_linux_distro()
self.assertEqual(('foo', '1.1', 'aarch64'), dist)
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 132f6051..718c6959 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -552,6 +552,11 @@ def is_FreeBSD():
return system_info()['variant'] == "freebsd"
+@lru_cache()
+def is_NetBSD():
+ return system_info()['variant'] == "netbsd"
+
+
def get_cfg_option_bool(yobj, key, default=False):
if key not in yobj:
return default
@@ -625,10 +630,9 @@ def get_linux_distro():
flavor = match.groupdict()['codename']
if distro_name == 'rhel':
distro_name = 'redhat'
- elif os.path.exists('/bin/freebsd-version'):
- distro_name = 'freebsd'
- distro_version, _ = subp(['uname', '-r'])
- distro_version = distro_version.strip()
+ elif 'BSD' in platform.system():
+ distro_name = platform.system().lower()
+ distro_version = platform.release()
else:
dist = ('', '', '')
try:
@@ -675,7 +679,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
@@ -1254,6 +1258,21 @@ def close_stdin():
os.dup2(fp.fileno(), sys.stdin.fileno())
+def find_devs_with_netbsd(criteria=None, oformat='device',
+ tag=None, no_cache=False, path=None):
+ if not path:
+ path = "/dev/cd0"
+ cmd = ["mscdlabel", path]
+ out, _ = subp(cmd, capture=True, decode="replace", rcs=[0, 1])
+ result = out.split()
+ if result and len(result) > 2:
+ if criteria == "TYPE=iso9660" and "ISO" in result:
+ return [path]
+ if criteria == "LABEL=CONFIG-2" and '"config-2"' in result:
+ return [path]
+ return []
+
+
def find_devs_with(criteria=None, oformat='device',
tag=None, no_cache=False, path=None):
"""
@@ -1263,6 +1282,10 @@ def find_devs_with(criteria=None, oformat='device',
LABEL=<label>
UUID=<uuid>
"""
+ if is_NetBSD():
+ return find_devs_with_netbsd(criteria, oformat,
+ tag, no_cache, path)
+
blk_id_cmd = ['blkid']
options = []
if criteria:
diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl
index 99f96ea1..50cfbb2f 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
@@ -33,7 +33,7 @@ ssh_pwauth: 0
# This will cause the set+update hostname module to not operate (if true)
preserve_hostname: false
-{% if variant in ["freebsd"] %}
+{% if variant in ["freebsd", "netbsd"] %}
# This should not be required, but leave it in place until the real cause of
# not finding -any- datasources is resolved.
datasource_list: ['NoCloud', 'ConfigDrive', 'Azure', 'OpenStack', 'Ec2']
@@ -55,18 +55,22 @@ network:
# The modules that run in the 'init' stage
cloud_init_modules:
- migrator
+{% if variant not in ["netbsd"] %}
- seed_random
+{% endif %}
- 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
@@ -100,7 +104,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
@@ -121,7 +125,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
@@ -143,7 +147,7 @@ cloud_final_modules:
# (not accessible to handlers/transforms)
system_info:
# This will affect which distro class gets used
-{% if variant in ["amazon", "arch", "centos", "debian", "fedora", "freebsd", "rhel", "suse", "ubuntu"] %}
+{% if variant in ["amazon", "arch", "centos", "debian", "fedora", "freebsd", "netbsd", "rhel", "suse", "ubuntu"] %}
distro: {{ variant }}
{% else %}
# Unknown/fallback distro.
@@ -226,4 +230,16 @@ 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", "netbsd"] %}
+ network:
+ renderers: ['{{ variant }}']
{% endif %}
diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst
index 0144dfae..ba00a889 100644
--- a/doc/rtd/topics/network-config.rst
+++ b/doc/rtd/topics/network-config.rst
@@ -197,7 +197,7 @@ supplying an updated configuration in cloud-config. ::
system_info:
network:
- renderers: ['netplan', 'eni', 'sysconfig', 'freebsd']
+ renderers: ['netplan', 'eni', 'sysconfig', 'freebsd', 'netbsd']
Network Configuration Tools
diff --git a/setup.py b/setup.py
index 01a67b95..69bb1a51 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',
@@ -272,7 +274,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 00000000..5cd7eb31
--- /dev/null
+++ b/sysvinit/netbsd/cloudconfig
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# PROVIDE: cloudconfig
+# REQUIRE: cloudinit
+# BEFORE: sshd
+
+$_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"
diff --git a/sysvinit/netbsd/cloudfinal b/sysvinit/netbsd/cloudfinal
new file mode 100755
index 00000000..72f3e472
--- /dev/null
+++ b/sysvinit/netbsd/cloudfinal
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+# PROVIDE: cloudfinal
+# REQUIRE: LOGIN cloudconfig
+
+$_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"
diff --git a/sysvinit/netbsd/cloudinit b/sysvinit/netbsd/cloudinit
new file mode 100755
index 00000000..266afc2a
--- /dev/null
+++ b/sysvinit/netbsd/cloudinit
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+# PROVIDE: cloudinit
+# REQUIRE: cloudinitlocal
+
+$_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"
diff --git a/sysvinit/netbsd/cloudinitlocal b/sysvinit/netbsd/cloudinitlocal
new file mode 100755
index 00000000..1f30e70b
--- /dev/null
+++ b/sysvinit/netbsd/cloudinitlocal
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# PROVIDE: cloudinitlocal
+# REQUIRE: NETWORKING
+
+# After NETWORKING because we don't want staticroute to wipe
+# the route set by the DHCP client toward the meta-data server.
+$_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"
diff --git a/tests/unittests/test_distros/test_bsd_utils.py b/tests/unittests/test_distros/test_bsd_utils.py
new file mode 100644
index 00000000..b38e4af5
--- /dev/null
+++ b/tests/unittests/test_distros/test_bsd_utils.py
@@ -0,0 +1,66 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import cloudinit.distros.bsd_utils as bsd_utils
+
+from cloudinit.tests.helpers import (CiTestCase, ExitStack, mock)
+
+RC_FILE = """
+if something; then
+ do something here
+fi
+hostname={hostname}
+"""
+
+
+class TestBsdUtils(CiTestCase):
+
+ def setUp(self):
+ super().setUp()
+ patches = ExitStack()
+ self.addCleanup(patches.close)
+
+ self.load_file = patches.enter_context(
+ mock.patch.object(bsd_utils.util, 'load_file'))
+
+ self.write_file = patches.enter_context(
+ mock.patch.object(bsd_utils.util, 'write_file'))
+
+ def test_get_rc_config_value(self):
+ self.load_file.return_value = 'hostname=foo\n'
+ self.assertEqual(bsd_utils.get_rc_config_value('hostname'), 'foo')
+ self.load_file.assert_called_with('/etc/rc.conf')
+
+ self.load_file.return_value = 'hostname=foo'
+ self.assertEqual(bsd_utils.get_rc_config_value('hostname'), 'foo')
+
+ self.load_file.return_value = 'hostname="foo"'
+ self.assertEqual(bsd_utils.get_rc_config_value('hostname'), 'foo')
+
+ self.load_file.return_value = "hostname='foo'"
+ self.assertEqual(bsd_utils.get_rc_config_value('hostname'), 'foo')
+
+ self.load_file.return_value = 'hostname=\'foo"'
+ self.assertEqual(bsd_utils.get_rc_config_value('hostname'), "'foo\"")
+
+ self.load_file.return_value = ''
+ self.assertEqual(bsd_utils.get_rc_config_value('hostname'), None)
+
+ self.load_file.return_value = RC_FILE.format(hostname='foo')
+ self.assertEqual(bsd_utils.get_rc_config_value('hostname'), "foo")
+
+ def test_set_rc_config_value_unchanged(self):
+ # bsd_utils.set_rc_config_value('hostname', 'foo')
+ # self.write_file.assert_called_with('/etc/rc.conf', 'hostname=foo\n')
+
+ self.load_file.return_value = RC_FILE.format(hostname='foo')
+ self.write_file.assert_not_called()
+
+ def test_set_rc_config_value(self):
+ bsd_utils.set_rc_config_value('hostname', 'foo')
+ self.write_file.assert_called_with('/etc/rc.conf', 'hostname=foo\n')
+
+ self.load_file.return_value = RC_FILE.format(hostname='foo')
+ bsd_utils.set_rc_config_value('hostname', 'bar')
+ self.write_file.assert_called_with(
+ '/etc/rc.conf',
+ RC_FILE.format(hostname='bar'))
diff --git a/tools/build-on-netbsd b/tools/build-on-netbsd
new file mode 100755
index 00000000..1e2b9cff
--- /dev/null
+++ b/tools/build-on-netbsd
@@ -0,0 +1,37 @@
+#!/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"
+
+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"
+cloudfinal="YES"' >> /etc/rc.conf
+
+echo "Installation completed."
diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg
index 3d5fa725..50f85369 100755
--- a/tools/render-cloudcfg
+++ b/tools/render-cloudcfg
@@ -4,8 +4,8 @@ import argparse
import os
import sys
-VARIANTS = ["amazon", "arch", "centos", "debian", "fedora", "freebsd", "rhel",
- "suse", "ubuntu", "unknown"]
+VARIANTS = ["amazon", "arch", "centos", "debian", "fedora", "freebsd", "netbsd",
+ "rhel", "suse", "ubuntu", "unknown"]
if "avoid-pep8-E402-import-not-top-of-file":
_tdir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))