Merge ~goneri/cloud-init:netbsd into cloud-init:master
- Git
- lp:~goneri/cloud-init
- netbsd
- Merge into master
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) |
Related bugs: |
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:/
- https:/
Description of the change
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
1 | diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py |
2 | index 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 |
63 | diff --git a/cloudinit/config/tests/test_set_passwords.py b/cloudinit/config/tests/test_set_passwords.py |
64 | index 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 |
99 | diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py |
100 | index 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'] |
126 | diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py |
127 | index 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() |
644 | diff --git a/cloudinit/distros/netbsd.py b/cloudinit/distros/netbsd.py |
645 | new file mode 100644 |
646 | index 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 |
892 | diff --git a/cloudinit/distros/netbsd_util.py b/cloudinit/distros/netbsd_util.py |
893 | new file mode 100644 |
894 | index 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 |
933 | diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py |
934 | index 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.""" |
980 | diff --git a/cloudinit/net/freebsd.py b/cloudinit/net/freebsd.py |
981 | new file mode 100644 |
982 | index 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() |
1110 | diff --git a/cloudinit/net/netbsd.py b/cloudinit/net/netbsd.py |
1111 | new file mode 100644 |
1112 | index 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 |
1245 | diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py |
1246 | index 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): |
1272 | diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py |
1273 | index 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) |
1342 | diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py |
1343 | index 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") |
1361 | diff --git a/cloudinit/util.py b/cloudinit/util.py |
1362 | index 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'): |
1393 | diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl |
1394 | index 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 %} |
1473 | diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst |
1474 | index 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 |
1486 | diff --git a/setup.py b/setup.py |
1487 | index 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']), |
1515 | diff --git a/sysvinit/netbsd/cloudconfig b/sysvinit/netbsd/cloudconfig |
1516 | new file mode 100755 |
1517 | index 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" |
1538 | diff --git a/sysvinit/netbsd/cloudfinal b/sysvinit/netbsd/cloudfinal |
1539 | new file mode 100755 |
1540 | index 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" |
1560 | diff --git a/sysvinit/netbsd/cloudinit b/sysvinit/netbsd/cloudinit |
1561 | new file mode 100755 |
1562 | index 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" |
1582 | diff --git a/sysvinit/netbsd/cloudinitlocal b/sysvinit/netbsd/cloudinitlocal |
1583 | new file mode 100755 |
1584 | index 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" |
1606 | diff --git a/tests/unittests/test_distros/test_generic.py b/tests/unittests/test_distros/test_generic.py |
1607 | index 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 |
1634 | diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py |
1635 | index 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): |
1863 | diff --git a/tools/build-on-freebsd b/tools/build-on-freebsd |
1864 | index 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 |
1875 | diff --git a/tools/build-on-netbsd b/tools/build-on-netbsd |
1876 | new file mode 100755 |
1877 | index 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." |
1921 | diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg |
1922 | index 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__), "..")) |