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

Proposed by Gonéri Le Bouder
Status: Work in progress
Proposed branch: ~goneri/cloud-init:netbsd
Merge into: cloud-init:master
Diff against target: 1933 lines (+945/-556)
25 files modified
cloudinit/config/cc_set_passwords.py (+16/-7)
cloudinit/config/tests/test_set_passwords.py (+19/-1)
cloudinit/distros/__init__.py (+8/-1)
cloudinit/distros/freebsd.py (+31/-420)
cloudinit/distros/netbsd.py (+242/-0)
cloudinit/distros/netbsd_util.py (+35/-0)
cloudinit/net/__init__.py (+36/-0)
cloudinit/net/freebsd.py (+124/-0)
cloudinit/net/netbsd.py (+129/-0)
cloudinit/net/renderers.py (+5/-1)
cloudinit/netinfo.py (+51/-1)
cloudinit/sources/DataSourceNoCloud.py (+8/-0)
cloudinit/util.py (+6/-1)
config/cloud.cfg.tmpl (+23/-6)
doc/rtd/topics/network-config.rst (+1/-1)
setup.py (+3/-1)
sysvinit/netbsd/cloudconfig (+17/-0)
sysvinit/netbsd/cloudfinal (+16/-0)
sysvinit/netbsd/cloudinit (+16/-0)
sysvinit/netbsd/cloudinitlocal (+18/-0)
tests/unittests/test_distros/test_generic.py (+18/-0)
tests/unittests/test_distros/test_netconfig.py (+82/-114)
tools/build-on-freebsd (+0/-1)
tools/build-on-netbsd (+40/-0)
tools/render-cloudcfg (+1/-1)
Reviewer Review Type Date Requested Status
cloud-init Commiters Pending
Review via email: mp+368508@code.launchpad.net

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

To post a comment you must log in.

Unmerged commits

4427531... by Gonéri Le Bouder

NetBSD support

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

- a NoCloud data source
- and NetBSD 7.0, 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

09ef518... by Gonéri Le Bouder

freebsd: introduce the freebsd renderer

Refactoring of the FreeBSD code base to provide a real network renderer
for FreeBSD.
Use the generic update_sysconfig_file() from rhel_util to handle the
access to /etc/rc.conf.
Interfaces are not automatically renamed by FreeBSD using
the following configuration in /etc/rc.conf:

```
ifconfig_fxp0_name="eth0"
```

34f5fe2... by Gonéri Le Bouder

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py
2index 1379428..9f71348 100755
3--- a/cloudinit/config/cc_set_passwords.py
4+++ b/cloudinit/config/cc_set_passwords.py
5@@ -179,20 +179,19 @@ def handle(_name, cfg, cloud, log, args):
6 for line in plist:
7 u, p = line.split(':', 1)
8 if prog.match(p) is not None and ":" not in p:
9- hashed_plist_in.append("%s:%s" % (u, p))
10+ hashed_plist_in.append(line)
11 hashed_users.append(u)
12 else:
13 if p == "R" or p == "RANDOM":
14 p = rand_user_password()
15- randlist.append("%s:%s" % (u, p))
16- plist_in.append("%s:%s" % (u, p))
17+ randlist.append(line)
18+ plist_in.append(line)
19 users.append(u)
20-
21 ch_in = '\n'.join(plist_in) + '\n'
22 if users:
23 try:
24 log.debug("Changing password for %s:", users)
25- util.subp(['chpasswd'], ch_in)
26+ chpasswd(cloud.distro, ch_in)
27 except Exception as e:
28 errors.append(e)
29 util.logexc(
30@@ -202,7 +201,7 @@ def handle(_name, cfg, cloud, log, args):
31 if hashed_users:
32 try:
33 log.debug("Setting hashed password for %s:", hashed_users)
34- util.subp(['chpasswd', '-e'], hashed_ch_in)
35+ chpasswd(cloud.distro, hashed_ch_in, hashed=True)
36 except Exception as e:
37 errors.append(e)
38 util.logexc(
39@@ -218,7 +217,7 @@ def handle(_name, cfg, cloud, log, args):
40 expired_users = []
41 for u in users:
42 try:
43- util.subp(['passwd', '--expire', u])
44+ cloud.distro.expire_passwd(u)
45 expired_users.append(u)
46 except Exception as e:
47 errors.append(e)
48@@ -238,4 +237,14 @@ def handle(_name, cfg, cloud, log, args):
49 def rand_user_password(pwlen=9):
50 return util.rand_str(pwlen, select_from=PW_SET)
51
52+
53+def chpasswd(distro, plist_in, hashed=False):
54+ if util.is_FreeBSD():
55+ for pentry in plist_in.splitlines():
56+ u, p = pentry.split(":")
57+ distro.set_passwd(u, p, hashed=hashed)
58+ else:
59+ cmd = ['chpasswd'] + ['-e'] if hashed else []
60+ util.subp(cmd, plist_in)
61+
62 # vi: ts=4 expandtab
63diff --git a/cloudinit/config/tests/test_set_passwords.py b/cloudinit/config/tests/test_set_passwords.py
64index a2ea5ec..c94e216 100644
65--- a/cloudinit/config/tests/test_set_passwords.py
66+++ b/cloudinit/config/tests/test_set_passwords.py
67@@ -74,7 +74,7 @@ class TestSetPasswordsHandle(CiTestCase):
68
69 with_logs = True
70
71- def test_handle_on_empty_config(self):
72+ def test_handle_on_empty_config(self, *args):
73 """handle logs that no password has changed when config is empty."""
74 cloud = self.tmp_cloud(distro='ubuntu')
75 setpass.handle(
76@@ -108,4 +108,22 @@ class TestSetPasswordsHandle(CiTestCase):
77 '\n'.join(valid_hashed_pwds) + '\n')],
78 m_subp.call_args_list)
79
80+ @mock.patch(MODPATH + "util.is_FreeBSD")
81+ @mock.patch(MODPATH + "util.subp")
82+ def test_freebsd_calls_custom_pw_cmds_to_set_and_expire_passwords(
83+ self, m_subp, m_is_freebsd):
84+ """FreeBSD calls custom pw commands instead of chpasswd and passwd"""
85+ m_is_freebsd.return_value = True
86+ cloud = self.tmp_cloud(distro='freebsd')
87+ valid_pwds = ['ubuntu:passw0rd']
88+ cfg = {'chpasswd': {'list': valid_pwds}}
89+ setpass.handle(
90+ 'IGNORED', cfg=cfg, cloud=cloud, log=self.logger, args=[])
91+ self.assertEqual([
92+ mock.call(['pw', 'usermod', 'ubuntu', '-h', '0'], data='passw0rd',
93+ logstring="chpasswd for ubuntu"),
94+ mock.call(['pw', 'usermod', 'ubuntu', '-p', '01-Jan-1970'])],
95+ m_subp.call_args_list)
96+
97+
98 # vi: ts=4 expandtab
99diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
100index 00bdee3..61ed2aa 100644
101--- a/cloudinit/distros/__init__.py
102+++ b/cloudinit/distros/__init__.py
103@@ -145,7 +145,7 @@ class Distro(object):
104 # Write it out
105
106 # pylint: disable=assignment-from-no-return
107- # We have implementations in arch, freebsd and gentoo still
108+ # We have implementations in arch and gentoo still
109 dev_names = self._write_network(settings)
110 # pylint: enable=assignment-from-no-return
111 # Now try to bring them up
112@@ -591,6 +591,13 @@ class Distro(object):
113 util.logexc(LOG, 'Failed to disable password for user %s', name)
114 raise e
115
116+ def expire_passwd(self, user):
117+ try:
118+ util.subp(['passwd', '--expire', user])
119+ except Exception as e:
120+ util.logexc(log, "Failed to set 'expire' for %s", user)
121+ raise e
122+
123 def set_passwd(self, user, passwd, hashed=False):
124 pass_string = '%s:%s' % (user, passwd)
125 cmd = ['chpasswd']
126diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py
127index f7825fd..2c10fbf 100644
128--- a/cloudinit/distros/freebsd.py
129+++ b/cloudinit/distros/freebsd.py
130@@ -13,24 +13,21 @@ import re
131 from cloudinit import distros
132 from cloudinit import helpers
133 from cloudinit import log as logging
134+from cloudinit import net
135 from cloudinit import ssh_util
136 from cloudinit import util
137-
138-from cloudinit.distros import net_util
139-from cloudinit.distros.parsers.resolv_conf import ResolvConf
140-
141+from cloudinit.distros import rhel_util
142 from cloudinit.settings import PER_INSTANCE
143
144 LOG = logging.getLogger(__name__)
145
146
147 class Distro(distros.Distro):
148- rc_conf_fn = "/etc/rc.conf"
149 login_conf_fn = '/etc/login.conf'
150 login_conf_fn_bak = '/etc/login.conf.orig'
151- resolv_conf_fn = '/etc/resolv.conf'
152 ci_sudoers_fn = '/usr/local/etc/sudoers.d/90-cloud-init-users'
153- default_primary_nic = 'hn0'
154+ rc_conf_fn = '/etc/rc.conf'
155+ hostname_conf_fn = '/etc/rc.conf'
156
157 def __init__(self, name, cfg, paths):
158 distros.Distro.__init__(self, name, cfg, paths)
159@@ -39,99 +36,8 @@ class Distro(distros.Distro):
160 # should only happen say once per instance...)
161 self._runner = helpers.Runners(paths)
162 self.osfamily = 'freebsd'
163- self.ipv4_pat = re.compile(r"\s+inet\s+\d+[.]\d+[.]\d+[.]\d+")
164 cfg['ssh_svcname'] = 'sshd'
165
166- # Updates a key in /etc/rc.conf.
167- def updatercconf(self, key, value):
168- LOG.debug("Checking %s for: %s = %s", self.rc_conf_fn, key, value)
169- conf = self.loadrcconf()
170- config_changed = False
171- if key not in conf:
172- LOG.debug("Adding key in %s: %s = %s", self.rc_conf_fn, key,
173- value)
174- conf[key] = value
175- config_changed = True
176- else:
177- for item in conf.keys():
178- if item == key and conf[item] != value:
179- conf[item] = value
180- LOG.debug("Changing key in %s: %s = %s", self.rc_conf_fn,
181- key, value)
182- config_changed = True
183-
184- if config_changed:
185- LOG.info("Writing %s", self.rc_conf_fn)
186- buf = StringIO()
187- for keyval in conf.items():
188- buf.write('%s="%s"\n' % keyval)
189- util.write_file(self.rc_conf_fn, buf.getvalue())
190-
191- # Load the contents of /etc/rc.conf and store all keys in a dict. Make sure
192- # quotes are ignored:
193- # hostname="bla"
194- def loadrcconf(self):
195- RE_MATCH = re.compile(r'^(\w+)\s*=\s*(.*)\s*')
196- conf = {}
197- lines = util.load_file(self.rc_conf_fn).splitlines()
198- for line in lines:
199- m = RE_MATCH.match(line)
200- if not m:
201- LOG.debug("Skipping line from /etc/rc.conf: %s", line)
202- continue
203- key = m.group(1).rstrip()
204- val = m.group(2).rstrip()
205- # Kill them quotes (not completely correct, aka won't handle
206- # quoted values, but should be ok ...)
207- if val[0] in ('"', "'"):
208- val = val[1:]
209- if val[-1] in ('"', "'"):
210- val = val[0:-1]
211- if len(val) == 0:
212- LOG.debug("Skipping empty value from /etc/rc.conf: %s", line)
213- continue
214- conf[key] = val
215- return conf
216-
217- def readrcconf(self, key):
218- conf = self.loadrcconf()
219- try:
220- val = conf[key]
221- except KeyError:
222- val = None
223- return val
224-
225- # NOVA will inject something like eth0, rewrite that to use the FreeBSD
226- # adapter. Since this adapter is based on the used driver, we need to
227- # figure out which interfaces are available. On KVM platforms this is
228- # vtnet0, where Xen would use xn0.
229- def getnetifname(self, dev):
230- LOG.debug("Translating network interface %s", dev)
231- if dev.startswith('lo'):
232- return dev
233-
234- n = re.search(r'\d+$', dev)
235- index = n.group(0)
236-
237- (out, _err) = util.subp(['ifconfig', '-a'])
238- ifconfigoutput = [x for x in (out.strip()).splitlines()
239- if len(x.split()) > 0]
240- bsddev = 'NOT_FOUND'
241- for line in ifconfigoutput:
242- m = re.match(r'^\w+', line)
243- if m:
244- if m.group(0).startswith('lo'):
245- continue
246- # Just settle with the first non-lo adapter we find, since it's
247- # rather unlikely there will be multiple nicdrivers involved.
248- bsddev = m.group(0)
249- break
250-
251- # Replace the index with the one we're after.
252- bsddev = re.sub(r'\d+$', index, bsddev)
253- LOG.debug("Using network interface %s", bsddev)
254- return bsddev
255-
256 def _select_hostname(self, hostname, fqdn):
257 # Should be FQDN if available. See rc.conf(5) in FreeBSD
258 if fqdn:
259@@ -139,21 +45,18 @@ class Distro(distros.Distro):
260 return hostname
261
262 def _read_system_hostname(self):
263- sys_hostname = self._read_hostname(filename=None)
264- return ('rc.conf', sys_hostname)
265+ sys_hostname = self._read_hostname(self.hostname_conf_fn)
266+ return (self.hostname_conf_fn, sys_hostname)
267
268 def _read_hostname(self, filename, default=None):
269- hostname = None
270- try:
271- hostname = self.readrcconf('hostname')
272- except IOError:
273- pass
274- if not hostname:
275+ (_exists, contents) = rhel_util.read_sysconfig_file(filename)
276+ if contents.get('hostname'):
277+ return contents['hostname']
278+ else:
279 return default
280- return hostname
281
282 def _write_hostname(self, hostname, filename):
283- self.updatercconf('hostname', hostname)
284+ rhel_util.update_sysconfig_file(filename, {'hostname': hostname})
285
286 def create_group(self, name, members):
287 group_add_cmd = ['pw', '-n', name]
288@@ -233,6 +136,13 @@ class Distro(distros.Distro):
289 if passwd_val is not None:
290 self.set_passwd(name, passwd_val, hashed=True)
291
292+ def expire_passwd(self, user):
293+ try:
294+ util.subp(['pw', 'usermod', user, '-p', '01-Jan-1970'])
295+ except Exception as e:
296+ util.logexc(log, "Failed to set pw expiration for %s", user)
297+ raise e
298+
299 def set_passwd(self, user, passwd, hashed=False):
300 if hashed:
301 hash_opt = "-H"
302@@ -274,309 +184,16 @@ class Distro(distros.Distro):
303 keys = set(kwargs['ssh_authorized_keys']) or []
304 ssh_util.setup_user_keys(keys, name, options=None)
305
306- @staticmethod
307- def get_ifconfig_list():
308- cmd = ['ifconfig', '-l']
309- (nics, err) = util.subp(cmd, rcs=[0, 1])
310- if len(err):
311- LOG.warning("Error running %s: %s", cmd, err)
312- return None
313- return nics
314-
315- @staticmethod
316- def get_ifconfig_ifname_out(ifname):
317- cmd = ['ifconfig', ifname]
318- (if_result, err) = util.subp(cmd, rcs=[0, 1])
319- if len(err):
320- LOG.warning("Error running %s: %s", cmd, err)
321- return None
322- return if_result
323-
324- @staticmethod
325- def get_ifconfig_ether():
326- cmd = ['ifconfig', '-l', 'ether']
327- (nics, err) = util.subp(cmd, rcs=[0, 1])
328- if len(err):
329- LOG.warning("Error running %s: %s", cmd, err)
330- return None
331- return nics
332-
333- @staticmethod
334- def get_interface_mac(ifname):
335- if_result = Distro.get_ifconfig_ifname_out(ifname)
336- for item in if_result.splitlines():
337- if item.find('ether ') != -1:
338- mac = str(item.split()[1])
339- if mac:
340- return mac
341-
342- @staticmethod
343- def get_devicelist():
344- nics = Distro.get_ifconfig_list()
345- return nics.split()
346-
347- @staticmethod
348- def get_ipv6():
349- ipv6 = []
350- nics = Distro.get_devicelist()
351- for nic in nics:
352- if_result = Distro.get_ifconfig_ifname_out(nic)
353- for item in if_result.splitlines():
354- if item.find("inet6 ") != -1 and item.find("scopeid") == -1:
355- ipv6.append(nic)
356- return ipv6
357-
358- def get_ipv4(self):
359- ipv4 = []
360- nics = Distro.get_devicelist()
361- for nic in nics:
362- if_result = Distro.get_ifconfig_ifname_out(nic)
363- for item in if_result.splitlines():
364- print(item)
365- if self.ipv4_pat.match(item):
366- ipv4.append(nic)
367- return ipv4
368-
369- def is_up(self, ifname):
370- if_result = Distro.get_ifconfig_ifname_out(ifname)
371- pat = "^" + ifname
372- for item in if_result.splitlines():
373- if re.match(pat, item):
374- flags = item.split('<')[1].split('>')[0]
375- if flags.find("UP") != -1:
376- return True
377-
378- def _get_current_rename_info(self, check_downable=True):
379- """Collect information necessary for rename_interfaces."""
380- names = Distro.get_devicelist()
381- bymac = {}
382- for n in names:
383- bymac[Distro.get_interface_mac(n)] = {
384- 'name': n, 'up': self.is_up(n), 'downable': None}
385-
386- nics_with_addresses = set()
387- if check_downable:
388- nics_with_addresses = set(self.get_ipv4() + self.get_ipv6())
389-
390- for d in bymac.values():
391- d['downable'] = (d['up'] is False or
392- d['name'] not in nics_with_addresses)
393-
394- return bymac
395-
396- def _rename_interfaces(self, renames):
397- if not len(renames):
398- LOG.debug("no interfaces to rename")
399- return
400-
401- current_info = self._get_current_rename_info()
402-
403- cur_bymac = {}
404- for mac, data in current_info.items():
405- cur = data.copy()
406- cur['mac'] = mac
407- cur_bymac[mac] = cur
408-
409- def update_byname(bymac):
410- return dict((data['name'], data)
411- for data in bymac.values())
412-
413- def rename(cur, new):
414- util.subp(["ifconfig", cur, "name", new], capture=True)
415-
416- def down(name):
417- util.subp(["ifconfig", name, "down"], capture=True)
418-
419- def up(name):
420- util.subp(["ifconfig", name, "up"], capture=True)
421-
422- ops = []
423- errors = []
424- ups = []
425- cur_byname = update_byname(cur_bymac)
426- tmpname_fmt = "cirename%d"
427- tmpi = -1
428-
429- for mac, new_name in renames:
430- cur = cur_bymac.get(mac, {})
431- cur_name = cur.get('name')
432- cur_ops = []
433- if cur_name == new_name:
434- # nothing to do
435- continue
436-
437- if not cur_name:
438- errors.append("[nic not present] Cannot rename mac=%s to %s"
439- ", not available." % (mac, new_name))
440- continue
441-
442- if cur['up']:
443- msg = "[busy] Error renaming mac=%s from %s to %s"
444- if not cur['downable']:
445- errors.append(msg % (mac, cur_name, new_name))
446- continue
447- cur['up'] = False
448- cur_ops.append(("down", mac, new_name, (cur_name,)))
449- ups.append(("up", mac, new_name, (new_name,)))
450-
451- if new_name in cur_byname:
452- target = cur_byname[new_name]
453- if target['up']:
454- msg = "[busy-target] Error renaming mac=%s from %s to %s."
455- if not target['downable']:
456- errors.append(msg % (mac, cur_name, new_name))
457- continue
458- else:
459- cur_ops.append(("down", mac, new_name, (new_name,)))
460-
461- tmp_name = None
462- while tmp_name is None or tmp_name in cur_byname:
463- tmpi += 1
464- tmp_name = tmpname_fmt % tmpi
465-
466- cur_ops.append(("rename", mac, new_name, (new_name, tmp_name)))
467- target['name'] = tmp_name
468- cur_byname = update_byname(cur_bymac)
469- if target['up']:
470- ups.append(("up", mac, new_name, (tmp_name,)))
471-
472- cur_ops.append(("rename", mac, new_name, (cur['name'], new_name)))
473- cur['name'] = new_name
474- cur_byname = update_byname(cur_bymac)
475- ops += cur_ops
476-
477- opmap = {'rename': rename, 'down': down, 'up': up}
478- if len(ops) + len(ups) == 0:
479- if len(errors):
480- LOG.debug("unable to do any work for renaming of %s", renames)
481- else:
482- LOG.debug("no work necessary for renaming of %s", renames)
483- else:
484- LOG.debug("achieving renaming of %s with ops %s",
485- renames, ops + ups)
486-
487- for op, mac, new_name, params in ops + ups:
488- try:
489- opmap.get(op)(*params)
490- except Exception as e:
491- errors.append(
492- "[unknown] Error performing %s%s for %s, %s: %s" %
493- (op, params, mac, new_name, e))
494- if len(errors):
495- raise Exception('\n'.join(errors))
496-
497- def apply_network_config_names(self, netcfg):
498- renames = []
499- for ent in netcfg.get('config', {}):
500- if ent.get('type') != 'physical':
501- continue
502- mac = ent.get('mac_address')
503- name = ent.get('name')
504- if not mac:
505- continue
506- renames.append([mac, name])
507- return self._rename_interfaces(renames)
508-
509- @classmethod
510 def generate_fallback_config(self):
511- nics = Distro.get_ifconfig_ether()
512- if nics is None:
513- LOG.debug("Fail to get network interfaces")
514- return None
515- potential_interfaces = nics.split()
516- connected = []
517- for nic in potential_interfaces:
518- pat = "^" + nic
519- if_result = Distro.get_ifconfig_ifname_out(nic)
520- for item in if_result.split("\n"):
521- if re.match(pat, item):
522- flags = item.split('<')[1].split('>')[0]
523- if flags.find("RUNNING") != -1:
524- connected.append(nic)
525- if connected:
526- potential_interfaces = connected
527- names = list(sorted(potential_interfaces))
528- default_pri_nic = Distro.default_primary_nic
529- if default_pri_nic in names:
530- names.remove(default_pri_nic)
531- names.insert(0, default_pri_nic)
532- target_name = None
533- target_mac = None
534- for name in names:
535- mac = Distro.get_interface_mac(name)
536- if mac:
537- target_name = name
538- target_mac = mac
539- break
540- if target_mac and target_name:
541- nconf = {'config': [], 'version': 1}
542+ nconf = {'config': [], 'version': 1}
543+ for mac, name in net.get_interfaces_by_mac().items():
544 nconf['config'].append(
545- {'type': 'physical', 'name': target_name,
546- 'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]})
547- return nconf
548- else:
549- return None
550-
551- def _write_network(self, settings):
552- entries = net_util.translate_network(settings)
553- nameservers = []
554- searchdomains = []
555- dev_names = entries.keys()
556- for (device, info) in entries.items():
557- # Skip the loopback interface.
558- if device.startswith('lo'):
559- continue
560-
561- dev = self.getnetifname(device)
562-
563- LOG.info('Configuring interface %s', dev)
564-
565- if info.get('bootproto') == 'static':
566- LOG.debug('Configuring dev %s with %s / %s', dev,
567- info.get('address'), info.get('netmask'))
568- # Configure an ipv4 address.
569- ifconfig = (info.get('address') + ' netmask ' +
570- info.get('netmask'))
571-
572- # Configure the gateway.
573- self.updatercconf('defaultrouter', info.get('gateway'))
574-
575- if 'dns-nameservers' in info:
576- nameservers.extend(info['dns-nameservers'])
577- if 'dns-search' in info:
578- searchdomains.extend(info['dns-search'])
579- else:
580- ifconfig = 'DHCP'
581-
582- self.updatercconf('ifconfig_' + dev, ifconfig)
583-
584- # Try to read the /etc/resolv.conf or just start from scratch if that
585- # fails.
586- try:
587- resolvconf = ResolvConf(util.load_file(self.resolv_conf_fn))
588- resolvconf.parse()
589- except IOError:
590- util.logexc(LOG, "Failed to parse %s, use new empty file",
591- self.resolv_conf_fn)
592- resolvconf = ResolvConf('')
593- resolvconf.parse()
594-
595- # Add some nameservers
596- for server in nameservers:
597- try:
598- resolvconf.add_nameserver(server)
599- except ValueError:
600- util.logexc(LOG, "Failed to add nameserver %s", server)
601-
602- # And add any searchdomains.
603- for domain in searchdomains:
604- try:
605- resolvconf.add_search_domain(domain)
606- except ValueError:
607- util.logexc(LOG, "Failed to add search domain %s", domain)
608- util.write_file(self.resolv_conf_fn, str(resolvconf), 0o644)
609+ {'type': 'physical', 'name': name,
610+ 'mac_address': mac, 'subnets': [{'type': 'dhcp'}]})
611+ return nconf
612
613- return dev_names
614+ def _write_network_config(self, netconfig):
615+ return self._supported_write_network_config(netconfig)
616
617 def apply_locale(self, locale, out_fn=None):
618 # Adjust the locals value to the new value
619@@ -604,18 +221,12 @@ class Distro(distros.Distro):
620 util.logexc(LOG, "Failed to restore %s backup",
621 self.login_conf_fn)
622
623- def _bring_up_interface(self, device_name):
624- if device_name.startswith('lo'):
625- return
626- dev = self.getnetifname(device_name)
627- cmd = ['/etc/rc.d/netif', 'start', dev]
628- LOG.debug("Attempting to bring up interface %s using command %s",
629- dev, cmd)
630- # This could return 1 when the interface has already been put UP by the
631- # OS. This is just fine.
632- (_out, err) = util.subp(cmd, rcs=[0, 1])
633- if len(err):
634- LOG.warning("Error running %s: %s", cmd, err)
635+ def apply_network_config_names(self, netconfig):
636+ # This is handled by the freebsd network renderer. It writes in
637+ # /etc/rc.conf a line with the following format:
638+ # ifconfig_OLDNAME_name=NEWNAME
639+ # FreeBSD network script will rename the interface automatically.
640+ return
641
642 def install_packages(self, pkglist):
643 self.update_package_sources()
644diff --git a/cloudinit/distros/netbsd.py b/cloudinit/distros/netbsd.py
645new file mode 100644
646index 0000000..01b7829
647--- /dev/null
648+++ b/cloudinit/distros/netbsd.py
649@@ -0,0 +1,242 @@
650+# Copyright (C) 2014 Harm Weites
651+# Copyright (C) 2019 Gonéri Le Bouder
652+#
653+# This file is part of cloud-init. See LICENSE file for license information.
654+
655+import crypt
656+import os
657+import six
658+from six import StringIO
659+
660+import re
661+
662+from cloudinit import distros
663+from cloudinit import helpers
664+from cloudinit import log as logging
665+from cloudinit import net
666+from cloudinit import ssh_util
667+from cloudinit import util
668+from cloudinit.distros import netbsd_util
669+from cloudinit.settings import PER_INSTANCE
670+
671+from cloudinit.distros.parsers.sys_conf import SysConf
672+
673+LOG = logging.getLogger(__name__)
674+
675+
676+class Distro(distros.Distro):
677+ hostname_conf_fn = '/etc/rc.conf'
678+ ci_sudoers_fn = '/usr/pkg/etc/sudoers.d/90-cloud-init-users'
679+
680+ def __init__(self, name, cfg, paths):
681+ distros.Distro.__init__(self, name, cfg, paths)
682+ # This will be used to restrict certain
683+ # calls from repeatly happening (when they
684+ # should only happen say once per instance...)
685+ self._runner = helpers.Runners(paths)
686+ self.osfamily = 'netbsd'
687+ cfg['ssh_svcname'] = 'sshd'
688+
689+
690+ def _select_hostname(self, hostname, fqdn):
691+ if fqdn:
692+ return fqdn
693+ return hostname
694+
695+ def _select_hostname(self, hostname, fqdn):
696+ return hostname
697+
698+ def _read_system_hostname(self):
699+ sys_hostname = self._read_hostname(filename='/etc/rc.conf')
700+ return ('/etc/rc.conf', sys_hostname)
701+
702+ def _read_hostname(self, filename, default=None):
703+ return netbsd_util.get_rc_config_value('hostname')
704+
705+ def _write_hostname(self, hostname, filename):
706+ netbsd_util.set_rc_config_value('hostname', hostname, fn='/etc/rc.conf')
707+
708+ def create_group(self, name, members):
709+ group_add_cmd = ['pw', '-n', name]
710+ if util.is_group(name):
711+ LOG.warning("Skipping creation of existing group '%s'", name)
712+ else:
713+ try:
714+ util.subp(group_add_cmd)
715+ LOG.info("Created new group %s", name)
716+ except Exception as e:
717+ util.logexc(LOG, "Failed to create group %s", name)
718+ raise e
719+
720+ if len(members) > 0:
721+ for member in members:
722+ if not util.is_user(member):
723+ LOG.warning("Unable to add group member '%s' to group '%s'"
724+ "; user does not exist.", member, name)
725+ continue
726+ try:
727+ util.subp(['pw', 'usermod', '-n', name, '-G', member])
728+ LOG.info("Added user '%s' to group '%s'", member, name)
729+ except Exception:
730+ util.logexc(LOG, "Failed to add user '%s' to group '%s'",
731+ member, name)
732+
733+ def add_user(self, name, **kwargs):
734+ if util.is_user(name):
735+ LOG.info("User %s already exists, skipping.", name)
736+ return False
737+
738+ adduser_cmd = ['useradd']
739+ log_adduser_cmd = ['useradd']
740+
741+ adduser_opts = {
742+ "homedir": '-d',
743+ "gecos": '-c',
744+ "primary_group": '-g',
745+ "groups": '-G',
746+ "shell": '-s',
747+ }
748+ adduser_flags = {
749+ "no_user_group": '--no-user-group',
750+ "system": '--system',
751+ "no_log_init": '--no-log-init',
752+ }
753+
754+ for key, val in kwargs.items():
755+ if (key in adduser_opts and val and
756+ isinstance(val, six.string_types)):
757+ adduser_cmd.extend([adduser_opts[key], val])
758+
759+ elif key in adduser_flags and val:
760+ adduser_cmd.append(adduser_flags[key])
761+ log_adduser_cmd.append(adduser_flags[key])
762+
763+ if not 'no_create_home' in kwargs or not 'system' in kwargs:
764+ adduser_cmd += ['-m']
765+ log_adduser_cmd += ['-m']
766+
767+ adduser_cmd += [name]
768+ log_adduser_cmd += [name]
769+
770+ # Run the command
771+ LOG.info("Adding user %s", name)
772+ try:
773+ util.subp(adduser_cmd, logstring=log_adduser_cmd)
774+ except Exception as e:
775+ util.logexc(LOG, "Failed to create user %s", name)
776+ raise e
777+ # Set the password if it is provided
778+ # For security consideration, only hashed passwd is assumed
779+ passwd_val = kwargs.get('passwd', None)
780+ if passwd_val is not None:
781+ self.set_passwd(name, passwd_val, hashed=True)
782+
783+ def set_passwd(self, user, password, hashed=False):
784+ if hashed:
785+ hashed_pw = password
786+ else:
787+ hashed_pw = crypt.crypt(password, crypt.mksalt(crypt.METHOD_BLOWFISH))
788+
789+ try:
790+ util.subp(['usermod', '-C', 'no', '-p', hashed_pw, user])
791+ except Exception as e:
792+ util.logexc(LOG, "Failed to set password for %s", user)
793+ raise e
794+
795+ def force_passwd_change(self, user):
796+ try:
797+ util.subp(['usermod', '-F', name])
798+ except Exception as e:
799+ util.logexc(LOG, "Failed to set pw expiration for %s", user)
800+ raise e
801+
802+ def lock_passwd(self, name):
803+ try:
804+ util.subp(['usermod', '-C', 'yes', name])
805+ except Exception as e:
806+ util.logexc(LOG, "Failed to lock user %s", name)
807+ raise e
808+
809+ def create_user(self, name, **kwargs):
810+ self.add_user(name, **kwargs)
811+
812+ # Set password if plain-text password provided and non-empty
813+ if 'plain_text_passwd' in kwargs and kwargs['plain_text_passwd']:
814+ self.set_passwd(name, kwargs['plain_text_passwd'])
815+
816+ # Default locking down the account. 'lock_passwd' defaults to True.
817+ # lock account unless lock_password is False.
818+ if kwargs.get('lock_passwd', True):
819+ self.lock_passwd(name)
820+
821+ # Configure sudo access
822+ if 'sudo' in kwargs and kwargs['sudo'] is not False:
823+ self.write_sudo_rules(name, kwargs['sudo'])
824+
825+ # Import SSH keys
826+ if 'ssh_authorized_keys' in kwargs:
827+ keys = set(kwargs['ssh_authorized_keys']) or []
828+ ssh_util.setup_user_keys(keys, name, options=None)
829+
830+ def generate_fallback_config(self):
831+ nconf = {'config': [], 'version': 1}
832+ for mac, name in net.get_interfaces_by_mac().items():
833+ nconf['config'].append(
834+ {'type': 'physical', 'name': name,
835+ 'mac_address': mac, 'subnets': [{'type': 'dhcp'}]})
836+ return nconf
837+
838+ def _write_network_config(self, netconfig):
839+ return self._supported_write_network_config(netconfig)
840+
841+ def apply_network_config_names(self, netconfig):
842+ # NetBSD cannot rename interfaces (and so simplify our life here)
843+ return
844+
845+ def install_packages(self, pkglist):
846+ self.update_package_sources()
847+ self.package_command('install', pkgs=pkglist)
848+
849+ def package_command(self, command, args=None, pkgs=None):
850+ if pkgs is None:
851+ pkgs = []
852+
853+ os_release, _ = util.subp(['uname', '-r'])
854+ os_arch, _ = util.subp(['uname', '-m'])
855+ e = os.environ.copy()
856+ e['PKG_PATH'] = 'http://cdn.netbsd.org/pub/pkgsrc/packages/NetBSD/%s/%s/All/' % (os_arch, os_release)
857+
858+ if command == 'install':
859+ cmd = ['pkg_add', '-U']
860+ elif command == 'remove':
861+ cmd = ['pkg_delete']
862+ if args and isinstance(args, str):
863+ cmd.append(args)
864+ elif args and isinstance(args, list):
865+ cmd.extend(args)
866+
867+ pkglist = util.expand_package_list('%s-%s', pkgs)
868+ cmd.extend(pkglist)
869+
870+ # Allow the output of this to flow outwards (ie not be captured)
871+ util.subp(cmd, env=e, capture=False)
872+
873+
874+ def apply_locale(self, locale, out_fn=None):
875+ pass
876+
877+ def set_timezone(self, tz):
878+ distros.set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz))
879+
880+ def update_package_sources(self):
881+ pass
882+
883+ def user_passwords(self, entries, format):
884+ for user, password in entries:
885+ self.set_passwd(user, password, bool(format == 'hashed'))
886+
887+ def user_passwords(self, entries, format):
888+ for user, password in entries:
889+ self.set_passwd(user, password, bool(format == 'hashed'))
890+
891+# vi: ts=4 expandtab
892diff --git a/cloudinit/distros/netbsd_util.py b/cloudinit/distros/netbsd_util.py
893new file mode 100644
894index 0000000..a6c5e97
895--- /dev/null
896+++ b/cloudinit/distros/netbsd_util.py
897@@ -0,0 +1,35 @@
898+# This file is part of cloud-init. See LICENSE file for license information.
899+
900+
901+from cloudinit import util
902+
903+
904+def get_rc_config_value(key, fn='/etc/rc.conf'):
905+ contents = {}
906+ for line in util.load_file(fn).splitlines():
907+ if '=' in line:
908+ k, v = line.split('=', 1)
909+ contents[k] = v
910+ return contents.get(key)
911+
912+def set_rc_config_value(key, value, fn='/etc/rc.conf'):
913+ lines = []
914+ done = False
915+ if ' ' in value:
916+ value = '"%s"' % value
917+ for line in util.load_file(fn).splitlines():
918+ if '=' in line:
919+ k, v = line.split('=', 1)
920+ if k == key:
921+ v = value
922+ done = True
923+ lines.append('='.join([k, v]))
924+ else:
925+ lines.append(line)
926+ if not done:
927+ lines.append('='.join([key, value]))
928+ with open(fn, 'w') as fd:
929+ fd.write('\n'.join(lines) + '\n')
930+
931+
932+# vi: ts=4 expandtab
933diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
934index bd80637..599a53e 100644
935--- a/cloudinit/net/__init__.py
936+++ b/cloudinit/net/__init__.py
937@@ -765,6 +765,42 @@ def get_ib_interface_hwaddr(ifname, ethernet_format):
938
939
940 def get_interfaces_by_mac():
941+ if util.is_FreeBSD():
942+ return get_interfaces_by_mac_on_freebsd()
943+ elif util.is_NetBSD():
944+ return get_interfaces_by_mac_on_netbsd()
945+ else:
946+ return get_interfaces_by_mac_on_linux()
947+
948+
949+def get_interfaces_by_mac_on_freebsd():
950+ ret = {}
951+ (out, _) = util.subp(['ifconfig', '-a'])
952+ blocks = re.split(r'(^\S+|\n\S+):', out)
953+ blocks.reverse()
954+ blocks.pop() # Ignore the first one
955+ while blocks:
956+ ifname = blocks.pop()
957+ m = re.search(r'ether\s([\da-f:]{17})', blocks.pop())
958+ if m and m.group(1):
959+ ret[m.group(1)] = ifname
960+ return ret
961+
962+def get_interfaces_by_mac_on_netbsd():
963+ ret = {}
964+ re_field_match = (
965+ r"(?P<ifname>\w+).*address:\s"
966+ "(?P<mac>([\da-f]{2}[:-]){5}([\da-f]{2})).*")
967+ (out, _) = util.subp(['ifconfig', '-a'])
968+ if_lines = re.sub(r'\n\s+', ' ', out).splitlines()
969+ for line in if_lines:
970+ m = re.match(re_field_match, line)
971+ if m:
972+ fields = m.groupdict()
973+ ret[fields['mac']] = fields['ifname']
974+ return ret
975+
976+def get_interfaces_by_mac_on_linux():
977 """Build a dictionary of tuples {mac: name}.
978
979 Bridges and any devices that have a 'stolen' mac are excluded."""
980diff --git a/cloudinit/net/freebsd.py b/cloudinit/net/freebsd.py
981new file mode 100644
982index 0000000..b2d60b7
983--- /dev/null
984+++ b/cloudinit/net/freebsd.py
985@@ -0,0 +1,124 @@
986+# This file is part of cloud-init. See LICENSE file for license information.
987+
988+import re
989+
990+from cloudinit import log as logging
991+from cloudinit import net
992+from cloudinit import util
993+from cloudinit.distros import rhel_util
994+from cloudinit.distros.parsers.resolv_conf import ResolvConf
995+
996+from . import renderer
997+
998+LOG = logging.getLogger(__name__)
999+
1000+
1001+class Renderer(renderer.Renderer):
1002+ resolv_conf_fn = 'etc/resolv.conf'
1003+ rc_conf_fn = 'etc/rc.conf'
1004+
1005+ def __init__(self, config=None):
1006+ if not config:
1007+ config = {}
1008+ self.dhcp_interfaces = []
1009+ self._postcmds = config.get('postcmds', True)
1010+
1011+ def _render_route(self, route, indent=""):
1012+ pass
1013+
1014+ def _render_iface(self, iface, render_hwaddress=False):
1015+ pass
1016+
1017+ def _write_network(self, settings, target=None):
1018+ nameservers = []
1019+ searchdomains = []
1020+ ifname_by_mac = net.get_interfaces_by_mac()
1021+ for interface in settings.iter_interfaces():
1022+ device_name = interface.get("name")
1023+ device_mac = interface.get("mac_address")
1024+ if device_name:
1025+ if re.match(r'^lo\d+$', device_name):
1026+ continue
1027+ if device_mac and device_name:
1028+ cur_name = ifname_by_mac[device_mac]
1029+ if not cur_name:
1030+ LOG.info('Cannot find any device with MAC %s', device_mac)
1031+ continue
1032+ if cur_name != device_name:
1033+ rhel_util.update_sysconfig_file(
1034+ util.target_path(target, self.rc_conf_fn), {
1035+ 'ifconfig_%s_name' % cur_name: device_name})
1036+ elif device_mac:
1037+ device_name = ifname_by_mac[device_mac]
1038+
1039+ subnet = interface.get("subnets", [])[0]
1040+ LOG.info('Configuring interface %s', device_name)
1041+
1042+ if subnet.get('type') == 'static':
1043+ LOG.debug('Configuring dev %s with %s / %s', device_name,
1044+ subnet.get('address'), subnet.get('netmask'))
1045+ # Configure an ipv4 address.
1046+ ifconfig = (subnet.get('address') + ' netmask ' +
1047+ subnet.get('netmask'))
1048+
1049+ # Configure the gateway.
1050+ rhel_util.update_sysconfig_file(
1051+ util.target_path(target, self.rc_conf_fn), {
1052+ 'defaultrouter': subnet.get('gateway')})
1053+
1054+ if 'dns_nameservers' in subnet:
1055+ nameservers.extend(subnet['dns_nameservers'])
1056+ if 'dns_search' in subnet:
1057+ searchdomains.extend(subnet['dns_search'])
1058+ else:
1059+ self.dhcp_interfaces.append(device_name)
1060+ ifconfig = 'DHCP'
1061+
1062+ rhel_util.update_sysconfig_file(
1063+ util.target_path(target, self.rc_conf_fn), {
1064+ 'ifconfig_' + device_name: ifconfig})
1065+
1066+ # Try to read the /etc/resolv.conf or just start from scratch if that
1067+ # fails.
1068+ try:
1069+ resolvconf = ResolvConf(util.load_file(self.resolv_conf_fn))
1070+ resolvconf.parse()
1071+ except IOError:
1072+ util.logexc(LOG, "Failed to parse %s, use new empty file",
1073+ self.resolv_conf_fn)
1074+ resolvconf = ResolvConf('')
1075+ resolvconf.parse()
1076+
1077+ # Add some nameservers
1078+ for server in nameservers:
1079+ try:
1080+ resolvconf.add_nameserver(server)
1081+ except ValueError:
1082+ util.logexc(LOG, "Failed to add nameserver %s", server)
1083+
1084+ # And add any searchdomains.
1085+ for domain in searchdomains:
1086+ try:
1087+ resolvconf.add_search_domain(domain)
1088+ except ValueError:
1089+ util.logexc(LOG, "Failed to add search domain %s", domain)
1090+ util.write_file(self.resolv_conf_fn, str(resolvconf), 0o644)
1091+ self.start_services()
1092+
1093+ def render_network_state(self, network_state, templates=None, target=None):
1094+ self._write_network(network_state, target=target)
1095+
1096+ def start_services(self):
1097+ if not self._postcmds:
1098+ LOG.debug("freebsd generate postcmd disabled")
1099+ return
1100+
1101+ util.subp(['service', 'netif', 'restart'], capture=True)
1102+ util.subp(['service', 'routing', 'restart'], capture=True)
1103+ for dhcp_interface in self.dhcp_interfaces:
1104+ util.subp(['service', 'dhclient', 'restart', dhcp_interface],
1105+ capture=True)
1106+
1107+
1108+def available(target=None):
1109+ return util.is_FreeBSD()
1110diff --git a/cloudinit/net/netbsd.py b/cloudinit/net/netbsd.py
1111new file mode 100644
1112index 0000000..1d0d982
1113--- /dev/null
1114+++ b/cloudinit/net/netbsd.py
1115@@ -0,0 +1,129 @@
1116+# This file is part of cloud-init. See LICENSE file for license information.
1117+
1118+import os
1119+import re
1120+
1121+from cloudinit import log as logging
1122+from cloudinit import util
1123+from cloudinit.distros import rhel_util
1124+from cloudinit.distros import netbsd_util
1125+from cloudinit.distros.parsers.resolv_conf import ResolvConf
1126+
1127+from . import renderer
1128+
1129+LOG = logging.getLogger(__name__)
1130+
1131+
1132+class Renderer(renderer.Renderer):
1133+ resolv_conf_fn = '/etc/resolv.conf'
1134+
1135+ def __init__(self, config=None):
1136+ if not config:
1137+ config = {}
1138+ self.dhcp_interfaces = []
1139+ self._postcmds = config.get('postcmds', True)
1140+
1141+ def _render_route(self, route, indent=""):
1142+ pass
1143+
1144+ def _render_iface(self, iface, render_hwaddress=False):
1145+ pass
1146+
1147+ def _ifconfig_a(self):
1148+ (out, _) = util.subp(['ifconfig', '-a'])
1149+ return out
1150+
1151+ def _get_ifname_by_mac(self, mac):
1152+ out = self._ifconfig_a()
1153+ blocks = re.split(r'(^\S+|\n\S+):', out)
1154+ blocks.reverse()
1155+ blocks.pop() # Ignore the first one
1156+ while blocks:
1157+ ifname = blocks.pop()
1158+ m = re.search(r'address:\s([\da-f:]{17})', blocks.pop())
1159+ if m and m.group(1) == mac:
1160+ return ifname
1161+
1162+ def _write_network(self, settings):
1163+ nameservers = []
1164+ searchdomains = []
1165+ for interface in settings.iter_interfaces():
1166+ device_mac = interface.get("mac_address")
1167+ device_name = interface.get("name")
1168+ if device_mac:
1169+ device_name = self._get_ifname_by_mac(device_mac)
1170+
1171+ subnet = interface.get("subnets", [])[0]
1172+ LOG.info('Configuring interface %s', device_name)
1173+
1174+ if subnet.get('type') == 'static':
1175+ LOG.debug('Configuring dev %s with %s / %s', device_name,
1176+ subnet.get('address'), subnet.get('netmask'))
1177+ # Configure an ipv4 address.
1178+ ifconfig = (subnet.get('address') + ' netmask ' +
1179+ subnet.get('netmask'))
1180+
1181+ # Configure the gateway.
1182+ if subnet.get('gateway'):
1183+ netbsd_util.set_rc_config_value(
1184+ 'defaultroute', subnet.get('gateway'))
1185+
1186+ if 'dns_nameservers' in subnet:
1187+ nameservers.extend(subnet['dns_nameservers'])
1188+ if 'dns_search' in subnet:
1189+ searchdomains.extend(subnet['dns_search'])
1190+ netbsd_util.set_rc_config_value('ifconfig_' + device_name, ifconfig)
1191+ else:
1192+ self.dhcp_interfaces.append(device_name)
1193+
1194+
1195+ if self.dhcp_interfaces:
1196+ netbsd_util.set_rc_config_value('dhcpcd', 'YES')
1197+ netbsd_util.set_rc_config_value('dhcpcd_flags', ' '.join(self.dhcp_interfaces))
1198+
1199+ # Try to read the /etc/resolv.conf or just start from scratch if that
1200+ # fails.
1201+ try:
1202+ resolvconf = ResolvConf(util.load_file(self.resolv_conf_fn))
1203+ resolvconf.parse()
1204+ except IOError:
1205+ util.logexc(LOG, "Failed to parse %s, use new empty file",
1206+ self.resolv_conf_fn)
1207+ resolvconf = ResolvConf('')
1208+ resolvconf.parse()
1209+
1210+ # Add some nameservers
1211+ for server in nameservers:
1212+ try:
1213+ resolvconf.add_nameserver(server)
1214+ except ValueError:
1215+ util.logexc(LOG, "Failed to add nameserver %s", server)
1216+
1217+ # And add any searchdomains.
1218+ for domain in searchdomains:
1219+ try:
1220+ resolvconf.add_search_domain(domain)
1221+ except ValueError:
1222+ util.logexc(LOG, "Failed to add search domain %s", domain)
1223+ util.write_file(self.resolv_conf_fn, str(resolvconf), 0o644)
1224+ self.start_services()
1225+
1226+ def render_network_state(self, network_state, templates=None, target=None):
1227+ self._write_network(network_state)
1228+
1229+ def start_services(self):
1230+ if not self._postcmds:
1231+ LOG.debug("netbsd generate postcmd disabled")
1232+ return
1233+
1234+ util.subp(['service', 'network', 'restart'], capture=True)
1235+ if self.dhcp_interfaces:
1236+ util.subp(['service', 'dhcpcd', 'restart'], capture=True)
1237+
1238+
1239+def available(target=None):
1240+ rcconf_path = util.target_path(target, 'etc/rc.conf')
1241+ if not os.path.isfile(rcconf_path):
1242+ return False
1243+
1244+ return True
1245diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py
1246index 5117b4a..e4bcae9 100644
1247--- a/cloudinit/net/renderers.py
1248+++ b/cloudinit/net/renderers.py
1249@@ -1,17 +1,21 @@
1250 # This file is part of cloud-init. See LICENSE file for license information.
1251
1252 from . import eni
1253+from . import freebsd
1254+from . import netbsd
1255 from . import netplan
1256 from . import RendererNotFoundError
1257 from . import sysconfig
1258
1259 NAME_TO_RENDERER = {
1260 "eni": eni,
1261+ "freebsd": freebsd,
1262+ "netbsd": netbsd,
1263 "netplan": netplan,
1264 "sysconfig": sysconfig,
1265 }
1266
1267-DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan"]
1268+DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan", "freebsd", "netbsd"]
1269
1270
1271 def search(priority=None, target=None, first=False):
1272diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
1273index e91cd26..2e4edbc 100644
1274--- a/cloudinit/netinfo.py
1275+++ b/cloudinit/netinfo.py
1276@@ -91,6 +91,53 @@ def _netdev_info_iproute(ipaddr_out):
1277 return devs
1278
1279
1280+def _netdev_info_ifconfig_netbsd(ifconfig_data):
1281+ # fields that need to be returned in devs for each dev
1282+ devs = {}
1283+ for line in ifconfig_data.splitlines():
1284+ if len(line) == 0:
1285+ continue
1286+ if line[0] not in ("\t", " "):
1287+ curdev = line.split()[0]
1288+ # current ifconfig pops a ':' on the end of the device
1289+ if curdev.endswith(':'):
1290+ curdev = curdev[:-1]
1291+ if curdev not in devs:
1292+ devs[curdev] = deepcopy(DEFAULT_NETDEV_INFO)
1293+ toks = line.lower().strip().split()
1294+ if len(toks) > 1:
1295+ if re.search(r"flags=[x\d]+<up.*>", toks[1]):
1296+ devs[curdev]['up'] = True
1297+
1298+ for i in range(len(toks)):
1299+ if toks[i] == "inet": # Create new ipv4 addr entry
1300+ network, net_bits = toks[i + 1].split('/')
1301+ devs[curdev]['ipv4'].append(
1302+ {'ip': network, 'mask': net_prefix_to_ipv4_mask(net_bits)})
1303+ elif toks[i] == "broadcast":
1304+ devs[curdev]['ipv4'][-1]['bcast'] = toks[i + 1]
1305+ elif toks[i] == "address:":
1306+ devs[curdev]['hwaddr'] = toks[i + 1]
1307+ elif toks[i] == "inet6":
1308+ if toks[i + 1] == "addr:":
1309+ devs[curdev]['ipv6'].append({'ip': toks[i + 2]})
1310+ else:
1311+ devs[curdev]['ipv6'].append({'ip': toks[i + 1]})
1312+ elif toks[i] == "prefixlen": # Add prefix to current ipv6 value
1313+ addr6 = devs[curdev]['ipv6'][-1]['ip'] + "/" + toks[i + 1]
1314+ devs[curdev]['ipv6'][-1]['ip'] = addr6
1315+ elif toks[i].startswith("scope:"):
1316+ devs[curdev]['ipv6'][-1]['scope6'] = toks[i].lstrip("scope:")
1317+ elif toks[i] == "scopeid":
1318+ res = re.match(r'.*<(\S+)>', toks[i + 1])
1319+ if res:
1320+ devs[curdev]['ipv6'][-1]['scope6'] = res.group(1)
1321+ else:
1322+ devs[curdev]['ipv6'][-1]['scope6'] = toks[i + 1]
1323+
1324+ return devs
1325+
1326+
1327 def _netdev_info_ifconfig(ifconfig_data):
1328 # fields that need to be returned in devs for each dev
1329 devs = {}
1330@@ -149,7 +196,10 @@ def _netdev_info_ifconfig(ifconfig_data):
1331
1332 def netdev_info(empty=""):
1333 devs = {}
1334- if util.which('ip'):
1335+ if util.is_NetBSD():
1336+ (ifcfg_out, _err) = util.subp(["ifconfig", "-a"], rcs=[0, 1])
1337+ devs = _netdev_info_ifconfig_netbsd(ifcfg_out)
1338+ elif util.which('ip'):
1339 # Try iproute first of all
1340 (ipaddr_out, _err) = util.subp(["ip", "addr", "show"])
1341 devs = _netdev_info_iproute(ipaddr_out)
1342diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py
1343index ee748b4..8418081 100644
1344--- a/cloudinit/sources/DataSourceNoCloud.py
1345+++ b/cloudinit/sources/DataSourceNoCloud.py
1346@@ -40,6 +40,14 @@ class DataSourceNoCloud(sources.DataSource):
1347 devlist = [
1348 p for p in ['/dev/msdosfs/' + label, '/dev/iso9660/' + label]
1349 if os.path.exists(p)]
1350+ elif util.is_NetBSD():
1351+ out, _err = util.subp(['sysctl', '-n', 'hw.disknames'], rcs=[0])
1352+ devlist = []
1353+ for dev in out.split():
1354+ mscdlabel_out, _ = util.subp(['mscdlabel', dev], rcs=[0])
1355+ if ('label "%s"' % label) in mscdlabel_out:
1356+ devlist.append('/dev/' + dev)
1357+ devlist.append('/dev/' + dev + 'a') # NetBSD 7
1358 else:
1359 # Query optical drive to get it in blkid cache for 2.6 kernels
1360 util.find_devs_with(path="/dev/sr0")
1361diff --git a/cloudinit/util.py b/cloudinit/util.py
1362index 0d338ca..8638c6f 100644
1363--- a/cloudinit/util.py
1364+++ b/cloudinit/util.py
1365@@ -551,6 +551,10 @@ def is_FreeBSD():
1366 return system_info()['variant'] == "freebsd"
1367
1368
1369+def is_NetBSD():
1370+ return system_info()['variant'] == "netbsd"
1371+
1372+
1373 def get_cfg_option_bool(yobj, key, default=False):
1374 if key not in yobj:
1375 return default
1376@@ -668,7 +672,7 @@ def system_info():
1377 var = 'suse'
1378 else:
1379 var = 'linux'
1380- elif system in ('windows', 'darwin', "freebsd"):
1381+ elif system in ('windows', 'darwin', "freebsd", "netbsd"):
1382 var = system
1383
1384 info['variant'] = var
1385@@ -2402,6 +2406,7 @@ def get_mount_info_freebsd(path):
1386 return "/dev/" + label_part, ret[2], ret[1]
1387
1388
1389+
1390 def get_device_info_from_zpool(zpool):
1391 # zpool has 10 second timeout waiting for /dev/zfs LP: #1760173
1392 if not os.path.exists('/dev/zfs'):
1393diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl
1394index 87c37ba..2ee77b4 100644
1395--- a/config/cloud.cfg.tmpl
1396+++ b/config/cloud.cfg.tmpl
1397@@ -2,7 +2,7 @@
1398 # The top level settings are used as module
1399 # and system configuration.
1400
1401-{% if variant in ["freebsd"] %}
1402+{% if variant in ["freebsd", "netbsd"] %}
1403 syslog_fix_perms: root:wheel
1404 {% elif variant in ["suse"] %}
1405 syslog_fix_perms: root:root
1406@@ -48,15 +48,17 @@ cloud_init_modules:
1407 - seed_random
1408 - bootcmd
1409 - write-files
1410+{% if variant not in ["netbsd"] %}
1411 - growpart
1412 - resizefs
1413-{% if variant not in ["freebsd"] %}
1414+{% endif %}
1415+{% if variant not in ["freebsd", "netbsd"] %}
1416 - disk_setup
1417 - mounts
1418 {% endif %}
1419 - set_hostname
1420 - update_hostname
1421-{% if variant not in ["freebsd"] %}
1422+{% if variant not in ["freebsd", "netbsd"] %}
1423 - update_etc_hosts
1424 - ca-certs
1425 - rsyslog
1426@@ -91,7 +93,7 @@ cloud_config_modules:
1427 {% if variant in ["suse"] %}
1428 - zypper-add-repo
1429 {% endif %}
1430-{% if variant not in ["freebsd"] %}
1431+{% if variant not in ["freebsd", "netbsd"] %}
1432 - ntp
1433 {% endif %}
1434 - timezone
1435@@ -115,7 +117,7 @@ cloud_final_modules:
1436 {% if variant in ["ubuntu", "unknown"] %}
1437 - ubuntu-drivers
1438 {% endif %}
1439-{% if variant not in ["freebsd"] %}
1440+{% if variant not in ["freebsd", "netbsd"] %}
1441 - puppet
1442 - chef
1443 - mcollective
1444@@ -137,7 +139,7 @@ cloud_final_modules:
1445 # (not accessible to handlers/transforms)
1446 system_info:
1447 # This will affect which distro class gets used
1448-{% if variant in ["arch", "centos", "debian", "fedora", "freebsd", "rhel", "suse", "ubuntu"] %}
1449+{% if variant in ["arch", "centos", "debian", "fedora", "freebsd", "rhel", "suse", "ubuntu", "netbsd"] %}
1450 distro: {{ variant }}
1451 {% else %}
1452 # Unknown/fallback distro.
1453@@ -214,4 +216,19 @@ system_info:
1454 groups: [wheel]
1455 sudo: ["ALL=(ALL) NOPASSWD:ALL"]
1456 shell: /bin/tcsh
1457+{% elif variant in ["netbsd"] %}
1458+ default_user:
1459+ name: netbsd
1460+ lock_passwd: True
1461+ gecos: NetBSD
1462+ groups: [wheel]
1463+ sudo: ["ALL=(ALL) NOPASSWD:ALL"]
1464+ shell: /bin/sh
1465+{% endif %}
1466+{% if variant in ["freebsd"] %}
1467+ network:
1468+ renderers: ['freebsd']
1469+{% elif variant in ["netbsd"] %}
1470+ network:
1471+ renderers: ['netbsd']
1472 {% endif %}
1473diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst
1474index 51ced4d..a07db5f 100644
1475--- a/doc/rtd/topics/network-config.rst
1476+++ b/doc/rtd/topics/network-config.rst
1477@@ -191,7 +191,7 @@ supplying an updated configuration in cloud-config. ::
1478
1479 system_info:
1480 network:
1481- renderers: ['netplan', 'eni', 'sysconfig']
1482+ renderers: ['netplan', 'eni', 'sysconfig', 'freebsd', 'netbsd']
1483
1484
1485 Network Configuration Tools
1486diff --git a/setup.py b/setup.py
1487index fcaf26f..a0b6ffe 100755
1488--- a/setup.py
1489+++ b/setup.py
1490@@ -136,6 +136,7 @@ if '--distro' in sys.argv:
1491 INITSYS_FILES = {
1492 'sysvinit': [f for f in glob('sysvinit/redhat/*') if is_f(f)],
1493 'sysvinit_freebsd': [f for f in glob('sysvinit/freebsd/*') if is_f(f)],
1494+ 'sysvinit_netbsd': [f for f in glob('sysvinit/netbsd/*') if is_f(f)],
1495 'sysvinit_deb': [f for f in glob('sysvinit/debian/*') if is_f(f)],
1496 'sysvinit_openrc': [f for f in glob('sysvinit/gentoo/*') if is_f(f)],
1497 'sysvinit_suse': [f for f in glob('sysvinit/suse/*') if is_f(f)],
1498@@ -152,6 +153,7 @@ INITSYS_FILES = {
1499 INITSYS_ROOTS = {
1500 'sysvinit': 'etc/rc.d/init.d',
1501 'sysvinit_freebsd': 'usr/local/etc/rc.d',
1502+ 'sysvinit_netbsd': 'usr/local/etc/rc.d',
1503 'sysvinit_deb': 'etc/init.d',
1504 'sysvinit_openrc': 'etc/init.d',
1505 'sysvinit_suse': 'etc/init.d',
1506@@ -259,7 +261,7 @@ data_files = [
1507 (USR + '/share/doc/cloud-init/examples/seed',
1508 [f for f in glob('doc/examples/seed/*') if is_f(f)]),
1509 ]
1510-if os.uname()[0] != 'FreeBSD':
1511+if os.uname()[0] not in ['FreeBSD', 'NetBSD']:
1512 data_files.extend([
1513 (ETC + '/NetworkManager/dispatcher.d/',
1514 ['tools/hook-network-manager']),
1515diff --git a/sysvinit/netbsd/cloudconfig b/sysvinit/netbsd/cloudconfig
1516new file mode 100755
1517index 0000000..5cd7eb3
1518--- /dev/null
1519+++ b/sysvinit/netbsd/cloudconfig
1520@@ -0,0 +1,17 @@
1521+#!/bin/sh
1522+
1523+# PROVIDE: cloudconfig
1524+# REQUIRE: cloudinit
1525+# BEFORE: sshd
1526+
1527+$_rc_subr_loaded . /etc/rc.subr
1528+
1529+name="cloudinit"
1530+start_cmd="start_cloud_init"
1531+start_cloud_init()
1532+{
1533+ /usr/pkg/bin/cloud-init modules --mode config
1534+}
1535+
1536+load_rc_config $name
1537+run_rc_command "$1"
1538diff --git a/sysvinit/netbsd/cloudfinal b/sysvinit/netbsd/cloudfinal
1539new file mode 100755
1540index 0000000..72f3e47
1541--- /dev/null
1542+++ b/sysvinit/netbsd/cloudfinal
1543@@ -0,0 +1,16 @@
1544+#!/bin/sh
1545+
1546+# PROVIDE: cloudfinal
1547+# REQUIRE: LOGIN cloudconfig
1548+
1549+$_rc_subr_loaded . /etc/rc.subr
1550+
1551+name="cloudinit"
1552+start_cmd="start_cloud_init"
1553+start_cloud_init()
1554+{
1555+ /usr/pkg/bin/cloud-init modules --mode final
1556+}
1557+
1558+load_rc_config $name
1559+run_rc_command "$1"
1560diff --git a/sysvinit/netbsd/cloudinit b/sysvinit/netbsd/cloudinit
1561new file mode 100755
1562index 0000000..266afc2
1563--- /dev/null
1564+++ b/sysvinit/netbsd/cloudinit
1565@@ -0,0 +1,16 @@
1566+#!/bin/sh
1567+
1568+# PROVIDE: cloudinit
1569+# REQUIRE: cloudinitlocal
1570+
1571+$_rc_subr_loaded . /etc/rc.subr
1572+
1573+name="cloudinit"
1574+start_cmd="start_cloud_init"
1575+start_cloud_init()
1576+{
1577+ /usr/pkg/bin/cloud-init init
1578+}
1579+
1580+load_rc_config $name
1581+run_rc_command "$1"
1582diff --git a/sysvinit/netbsd/cloudinitlocal b/sysvinit/netbsd/cloudinitlocal
1583new file mode 100755
1584index 0000000..1f30e70
1585--- /dev/null
1586+++ b/sysvinit/netbsd/cloudinitlocal
1587@@ -0,0 +1,18 @@
1588+#!/bin/sh
1589+
1590+# PROVIDE: cloudinitlocal
1591+# REQUIRE: NETWORKING
1592+
1593+# After NETWORKING because we don't want staticroute to wipe
1594+# the route set by the DHCP client toward the meta-data server.
1595+$_rc_subr_loaded . /etc/rc.subr
1596+
1597+name="cloudinitlocal"
1598+start_cmd="start_cloud_init_local"
1599+start_cloud_init_local()
1600+{
1601+ /usr/pkg/bin/cloud-init init -l
1602+}
1603+
1604+load_rc_config $name
1605+run_rc_command "$1"
1606diff --git a/tests/unittests/test_distros/test_generic.py b/tests/unittests/test_distros/test_generic.py
1607index 791fe61..7e0da4f 100644
1608--- a/tests/unittests/test_distros/test_generic.py
1609+++ b/tests/unittests/test_distros/test_generic.py
1610@@ -244,5 +244,23 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase):
1611 with self.assertRaises(NotImplementedError):
1612 d.get_locale()
1613
1614+ def test_expire_passwd_uses_chpasswd(self):
1615+ """Test ubuntu.expire_passwd uses the passwd command."""
1616+ for d_name in ("ubuntu", "rhel"):
1617+ cls = distros.fetch(d_name)
1618+ d = cls(d_name, {}, None)
1619+ with mock.patch("cloudinit.util.subp") as m_subp:
1620+ d.expire_passwd("myuser")
1621+ m_subp.assert_called_once_with(["passwd", "--expire", "myuser"])
1622+
1623+ def test_expire_passwd_freebsd_uses_pw_command(self):
1624+ """Test FreeBSD.expire_passwd uses the pw command."""
1625+ cls = distros.fetch("freebsd")
1626+ d = cls("freebsd", {}, None)
1627+ with mock.patch("cloudinit.util.subp") as m_subp:
1628+ d.expire_passwd("myuser")
1629+ m_subp.assert_called_once_with(
1630+ ["pw", "usermod", "myuser", "-p", "01-Jan-1970"])
1631+
1632
1633 # vi: ts=4 expandtab
1634diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
1635index 6720995..4c14d8c 100644
1636--- a/tests/unittests/test_distros/test_netconfig.py
1637+++ b/tests/unittests/test_distros/test_netconfig.py
1638@@ -1,5 +1,6 @@
1639 # This file is part of cloud-init. See LICENSE file for license information.
1640
1641+import copy
1642 import os
1643 from six import StringIO
1644 from textwrap import dedent
1645@@ -14,7 +15,7 @@ from cloudinit.distros.parsers.sys_conf import SysConf
1646 from cloudinit import helpers
1647 from cloudinit import settings
1648 from cloudinit.tests.helpers import (
1649- FilesystemMockingTestCase, dir2dict, populate_dir)
1650+ FilesystemMockingTestCase, dir2dict)
1651 from cloudinit import util
1652
1653
1654@@ -213,128 +214,95 @@ class TestNetCfgDistroBase(FilesystemMockingTestCase):
1655 self.assertEqual(v, b2[k])
1656
1657
1658-class TestNetCfgDistroFreebsd(TestNetCfgDistroBase):
1659+class TestNetCfgDistroFreeBSD(TestNetCfgDistroBase):
1660
1661- frbsd_ifout = """\
1662-hn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
1663- options=51b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,TSO4,LRO>
1664- ether 00:15:5d:4c:73:00
1665- inet6 fe80::215:5dff:fe4c:7300%hn0 prefixlen 64 scopeid 0x2
1666- inet 10.156.76.127 netmask 0xfffffc00 broadcast 10.156.79.255
1667- nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL>
1668- media: Ethernet autoselect (10Gbase-T <full-duplex>)
1669- status: active
1670+ def setUp(self):
1671+ super(TestNetCfgDistroFreeBSD, self).setUp()
1672+ self.distro = self._get_distro('freebsd', renderers=['freebsd'])
1673+
1674+ def _apply_and_verify_freebsd(self, apply_fn, config, expected_cfgs=None,
1675+ bringup=False):
1676+ if not expected_cfgs:
1677+ raise ValueError('expected_cfg must not be None')
1678+
1679+ tmpd = None
1680+ with mock.patch('cloudinit.net.freebsd.available') as m_avail:
1681+ m_avail.return_value = True
1682+ with self.reRooted(tmpd) as tmpd:
1683+ util.ensure_dir('/etc')
1684+ util.ensure_file('/etc/rc.conf')
1685+ util.ensure_file('/etc/resolv.conf')
1686+ apply_fn(config, bringup)
1687+
1688+ results = dir2dict(tmpd)
1689+ for cfgpath, expected in expected_cfgs.items():
1690+ print("----------")
1691+ print(expected)
1692+ print("^^^^ expected | rendered VVVVVVV")
1693+ print(results[cfgpath])
1694+ print("----------")
1695+ self.assertEqual(
1696+ set(expected.split('\n')),
1697+ set(results[cfgpath].split('\n')))
1698+ self.assertEqual(0o644, get_mode(cfgpath, tmpd))
1699+
1700+ @mock.patch('cloudinit.net.get_interfaces_by_mac')
1701+ def test_apply_network_config_freebsd_standard(self, ifaces_mac):
1702+ ifaces_mac.return_value = {
1703+ '00:15:5d:4c:73:00': 'eth0',
1704+ }
1705+ rc_conf_expected = """\
1706+defaultrouter=192.168.1.254
1707+ifconfig_eth0='192.168.1.5 netmask 255.255.255.0'
1708+ifconfig_eth1=DHCP
1709 """
1710
1711- @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_list')
1712- @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ifname_out')
1713- def test_get_ip_nic_freebsd(self, ifname_out, iflist):
1714- frbsd_distro = self._get_distro('freebsd')
1715- iflist.return_value = "lo0 hn0"
1716- ifname_out.return_value = self.frbsd_ifout
1717- res = frbsd_distro.get_ipv4()
1718- self.assertEqual(res, ['lo0', 'hn0'])
1719- res = frbsd_distro.get_ipv6()
1720- self.assertEqual(res, [])
1721-
1722- @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ether')
1723- @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ifname_out')
1724- @mock.patch('cloudinit.distros.freebsd.Distro.get_interface_mac')
1725- def test_generate_fallback_config_freebsd(self, mac, ifname_out, if_ether):
1726- frbsd_distro = self._get_distro('freebsd')
1727-
1728- if_ether.return_value = 'hn0'
1729- ifname_out.return_value = self.frbsd_ifout
1730- mac.return_value = '00:15:5d:4c:73:00'
1731- res = frbsd_distro.generate_fallback_config()
1732- self.assertIsNotNone(res)
1733-
1734- def test_simple_write_freebsd(self):
1735- fbsd_distro = self._get_distro('freebsd')
1736-
1737- rc_conf = '/etc/rc.conf'
1738- read_bufs = {
1739- rc_conf: 'initial-rc-conf-not-validated',
1740- '/etc/resolv.conf': 'initial-resolv-conf-not-validated',
1741+ expected_cfgs = {
1742+ '/etc/rc.conf': rc_conf_expected,
1743+ '/etc/resolv.conf': ''
1744+ }
1745+ self._apply_and_verify_freebsd(self.distro.apply_network_config,
1746+ V1_NET_CFG,
1747+ expected_cfgs=expected_cfgs.copy())
1748+
1749+ @mock.patch('cloudinit.net.get_interfaces_by_mac')
1750+ def test_apply_network_config_freebsd_ifrename(self, ifaces_mac):
1751+ ifaces_mac.return_value = {
1752+ '00:15:5d:4c:73:00': 'vtnet0',
1753 }
1754+ rc_conf_expected = """\
1755+ifconfig_vtnet0_name=eth0
1756+defaultrouter=192.168.1.254
1757+ifconfig_eth0='192.168.1.5 netmask 255.255.255.0'
1758+ifconfig_eth1=DHCP
1759+"""
1760
1761- tmpd = self.tmp_dir()
1762- populate_dir(tmpd, read_bufs)
1763- with self.reRooted(tmpd):
1764- with mock.patch("cloudinit.distros.freebsd.util.subp",
1765- return_value=('vtnet0', '')):
1766- fbsd_distro.apply_network(BASE_NET_CFG, False)
1767- results = dir2dict(tmpd)
1768-
1769- self.assertIn(rc_conf, results)
1770- self.assertCfgEquals(
1771- dedent('''\
1772- ifconfig_vtnet0="192.168.1.5 netmask 255.255.255.0"
1773- ifconfig_vtnet1="DHCP"
1774- defaultrouter="192.168.1.254"
1775- '''), results[rc_conf])
1776- self.assertEqual(0o644, get_mode(rc_conf, tmpd))
1777-
1778- def test_simple_write_freebsd_from_v2eni(self):
1779- fbsd_distro = self._get_distro('freebsd')
1780-
1781- rc_conf = '/etc/rc.conf'
1782- read_bufs = {
1783- rc_conf: 'initial-rc-conf-not-validated',
1784- '/etc/resolv.conf': 'initial-resolv-conf-not-validated',
1785+ V1_NET_CFG_RENAME = copy.deepcopy(V1_NET_CFG)
1786+ V1_NET_CFG_RENAME['config'][0]['mac_address'] = '00:15:5d:4c:73:00'
1787+
1788+ expected_cfgs = {
1789+ '/etc/rc.conf': rc_conf_expected,
1790+ '/etc/resolv.conf': ''
1791 }
1792+ self._apply_and_verify_freebsd(self.distro.apply_network_config,
1793+ V1_NET_CFG_RENAME,
1794+ expected_cfgs=expected_cfgs.copy())
1795
1796- tmpd = self.tmp_dir()
1797- populate_dir(tmpd, read_bufs)
1798- with self.reRooted(tmpd):
1799- with mock.patch("cloudinit.distros.freebsd.util.subp",
1800- return_value=('vtnet0', '')):
1801- fbsd_distro.apply_network(BASE_NET_CFG_FROM_V2, False)
1802- results = dir2dict(tmpd)
1803-
1804- self.assertIn(rc_conf, results)
1805- self.assertCfgEquals(
1806- dedent('''\
1807- ifconfig_vtnet0="192.168.1.5 netmask 255.255.255.0"
1808- ifconfig_vtnet1="DHCP"
1809- defaultrouter="192.168.1.254"
1810- '''), results[rc_conf])
1811- self.assertEqual(0o644, get_mode(rc_conf, tmpd))
1812-
1813- def test_apply_network_config_fallback_freebsd(self):
1814- fbsd_distro = self._get_distro('freebsd')
1815-
1816- # a weak attempt to verify that we don't have an implementation
1817- # of _write_network_config or apply_network_config in fbsd now,
1818- # which would make this test not actually test the fallback.
1819- self.assertRaises(
1820- NotImplementedError, fbsd_distro._write_network_config,
1821- BASE_NET_CFG)
1822-
1823- # now run
1824- mynetcfg = {
1825- 'config': [{"type": "physical", "name": "eth0",
1826- "mac_address": "c0:d6:9f:2c:e8:80",
1827- "subnets": [{"type": "dhcp"}]}],
1828- 'version': 1}
1829-
1830- rc_conf = '/etc/rc.conf'
1831- read_bufs = {
1832- rc_conf: 'initial-rc-conf-not-validated',
1833- '/etc/resolv.conf': 'initial-resolv-conf-not-validated',
1834+ @mock.patch('cloudinit.net.get_interfaces_by_mac')
1835+ def test_apply_network_config_freebsd_nameserver(self, ifaces_mac):
1836+ ifaces_mac.return_value = {
1837+ '00:15:5d:4c:73:00': 'eth0',
1838 }
1839
1840- tmpd = self.tmp_dir()
1841- populate_dir(tmpd, read_bufs)
1842- with self.reRooted(tmpd):
1843- with mock.patch("cloudinit.distros.freebsd.util.subp",
1844- return_value=('vtnet0', '')):
1845- fbsd_distro.apply_network_config(mynetcfg, bring_up=False)
1846- results = dir2dict(tmpd)
1847-
1848- self.assertIn(rc_conf, results)
1849- self.assertCfgEquals('ifconfig_vtnet0="DHCP"', results[rc_conf])
1850- self.assertEqual(0o644, get_mode(rc_conf, tmpd))
1851+ V1_NET_CFG_DNS = copy.deepcopy(V1_NET_CFG)
1852+ ns = ['1.2.3.4']
1853+ V1_NET_CFG_DNS['config'][0]['subnets'][0]['dns_nameservers'] = ns
1854+ expected_cfgs = {
1855+ '/etc/resolv.conf': 'nameserver 1.2.3.4\n'
1856+ }
1857+ self._apply_and_verify_freebsd(self.distro.apply_network_config,
1858+ V1_NET_CFG_DNS,
1859+ expected_cfgs=expected_cfgs.copy())
1860
1861
1862 class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase):
1863diff --git a/tools/build-on-freebsd b/tools/build-on-freebsd
1864index 8ae6456..876368a 100755
1865--- a/tools/build-on-freebsd
1866+++ b/tools/build-on-freebsd
1867@@ -18,7 +18,6 @@ py_prefix=$(${PYTHON} -c 'import sys; print("py%d%d" % (sys.version_info.major,
1868 depschecked=/tmp/c-i.dependencieschecked
1869 pkgs="
1870 bash
1871- chpasswd
1872 dmidecode
1873 e2fsprogs
1874 $py_prefix-Jinja2
1875diff --git a/tools/build-on-netbsd b/tools/build-on-netbsd
1876new file mode 100755
1877index 0000000..97b6cf0
1878--- /dev/null
1879+++ b/tools/build-on-netbsd
1880@@ -0,0 +1,40 @@
1881+#!/bin/sh
1882+
1883+fail() { echo "FAILED:" "$@" 1>&2; exit 1; }
1884+
1885+# Check dependencies:
1886+depschecked=/tmp/c-i.dependencieschecked
1887+pkgs="
1888+ bash
1889+ dmidecode
1890+ py37-configobj
1891+ py37-jinja2
1892+ py37-oauthlib
1893+ py37-requests
1894+ py37-setuptools
1895+ py37-six
1896+ py37-yaml
1897+ sudo
1898+"
1899+[ -f "$depschecked" ] || pkg_add ${pkgs} || fail "install packages"
1900+
1901+pkg_add py37-pip
1902+pip3.7 --no-cache-dir install jsonpatch
1903+pip3.7 --no-cache-dir install jsonschema
1904+touch $depschecked
1905+
1906+# Build the code and install in /usr/pkg/:
1907+python3.7 setup.py build
1908+python3.7 setup.py install -O1 --distro netbsd --skip-build --init-system sysvinit_netbsd
1909+mv -v /usr/local/etc/rc.d/cloud* /etc/rc.d
1910+
1911+# Enable cloud-init in /etc/rc.conf:
1912+sed -i.bak -e "/^cloud.*=.*/d" /etc/rc.conf
1913+echo '
1914+# You can safely remove the following lines starting with "cloud"
1915+cloudinitlocal="YES"
1916+cloudinit="YES"
1917+cloudconfig="YES"
1918+cloudinitlocal="YES"' >> /etc/rc.conf
1919+
1920+echo "Installation completed."
1921diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg
1922index a441f4f..32ccfd6 100755
1923--- a/tools/render-cloudcfg
1924+++ b/tools/render-cloudcfg
1925@@ -5,7 +5,7 @@ import os
1926 import sys
1927
1928 VARIANTS = ["arch", "centos", "debian", "fedora", "freebsd", "rhel", "suse",
1929- "ubuntu", "unknown"]
1930+ "ubuntu", "unknown", "netbsd"]
1931
1932 if "avoid-pep8-E402-import-not-top-of-file":
1933 _tdir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))

Subscribers

People subscribed via source and target branches