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__), ".."))
_______________________________________________ Mailing list: https://launchpad.net/~cloud-init-dev Post to : cloud-init-dev@lists.launchpad.net Unsubscribe : https://launchpad.net/~cloud-init-dev More help : https://help.launchpad.net/ListHelp