cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #06315
[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