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

Proposed by Gonéri Le Bouder
Status: Needs review
Proposed branch: ~goneri/cloud-init:freebsd_net_renderer
Merge into: cloud-init:master
Diff against target: 998 lines (+292/-537)
9 files modified
cloudinit/distros/__init__.py (+1/-1)
cloudinit/distros/freebsd.py (+23/-419)
cloudinit/net/__init__.py (+30/-0)
cloudinit/net/freebsd.py (+123/-0)
cloudinit/net/renderers.py (+3/-1)
doc/rtd/topics/network-config.rst (+1/-1)
tests/unittests/test_distros/test_netconfig.py (+82/-114)
tests/unittests/test_net.py (+0/-1)
tests/unittests/test_net_freebsd.py (+29/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Needs Fixing
Mina Galić (community) Needs Fixing
Ryan Harper Needs Information
Review via email: mp+365641@code.launchpad.net

Commit message

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"
```

To post a comment you must log in.
Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for refactoring the BSD support. I've a few comments inline.

review: Needs Fixing
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

I pushed an update to address your comments.

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks. I've added a few more questions/comments inline.

review: Needs Fixing
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

Thanks Ryan for the review.

Revision history for this message
Gonéri Le Bouder (goneri) :
Revision history for this message
Ryan Harper (raharper) :
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

Hi Ryan,

I think I've address all your comments. I prefer to keep the interface renaming unchanged for now because:

- the current code base is complex and Linux specific, it means the logic has to be full reimplemented
- the current implementation is already broken, and nobody complains so I'm not afraid my patch will bring any regression
- the new implementation will cover most of the use-cases and follow FreeBSD philosophy.

So, I think the current patch already bring some good by reducing the complexity of the code base, and worth to be merge, even if this feature is not fully functional.

It may be interesting to revisit the renaming issue the day https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=118111 is merged.

Revision history for this message
Ryan Harper (raharper) :
review: Needs Fixing
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

> Ryan, could you elaborate on what needs to be fixed?

Oh, I just realized you answered with inline comments. sorry.

Revision history for this message
Gonéri Le Bouder (goneri) wrote :

Ryan, I think I've addressed all the comments.

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for the update. I've added some additional comments inline. Thanks for continue to work on this.

review: Needs Information
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

Done

Revision history for this message
Andrey Fesenko (f0andrey) wrote :

Thanks it's fix first start network.

I'm submit update in port FreeBSD
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=240372

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~goneri/cloud-init/+git/cloud-init/+merge/365641/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1123/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1123//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~goneri/cloud-init/+git/cloud-init/+merge/365641/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1132/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1132//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~goneri/cloud-init/+git/cloud-init/+merge/365641/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1133/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1133//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~goneri/cloud-init/+git/cloud-init/+merge/365641/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1134/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1134//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~goneri/cloud-init/+git/cloud-init/+merge/365641/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1135/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1135//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:fb42c3a3a00d9700957cbf4269d35ad0b7d161d8
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1137/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1137//rebuild

review: Approve (continuous-integration)
Revision history for this message
Mina Galić (minagalic) wrote :

i've had a look and tried the commands locally, and it appears this won't reliably work on any installation.

review: Needs Fixing
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

You are a bit harsh because, you don't point any major problem in the patch. I've updated it to take care of the colon in the interface name, but I don't think this is something really common. Do you actually have an ifconfig output example?

I will continue the review here:
https://github.com/canonical/cloud-init/pull/61

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:0f6af36387dc15ba9ddd72b56fdb81b5208f9568
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1287/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1287//rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Mina Galić (minagalic) wrote :

please forgive my harsh tone, i only now realised my comment is saying "on any installation" instead of "on every" 🙇‍♀️

Unmerged commits

0f6af36... 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"
```

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index 00bdee3..75bb441 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -145,7 +145,7 @@ class Distro(object):
145 # Write it out145 # Write it out
146146
147 # pylint: disable=assignment-from-no-return147 # pylint: disable=assignment-from-no-return
148 # We have implementations in arch, freebsd and gentoo still148 # We have implementations in arch and gentoo still
149 dev_names = self._write_network(settings)149 dev_names = self._write_network(settings)
150 # pylint: enable=assignment-from-no-return150 # pylint: enable=assignment-from-no-return
151 # Now try to bring them up151 # Now try to bring them up
diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py
index c55f899..3224887 100644
--- a/cloudinit/distros/freebsd.py
+++ b/cloudinit/distros/freebsd.py
@@ -13,12 +13,10 @@ import re
13from cloudinit import distros13from cloudinit import distros
14from cloudinit import helpers14from cloudinit import helpers
15from cloudinit import log as logging15from cloudinit import log as logging
16from cloudinit import net
16from cloudinit import ssh_util17from cloudinit import ssh_util
17from cloudinit import util18from cloudinit import util
1819from cloudinit.distros import rhel_util
19from cloudinit.distros import net_util
20from cloudinit.distros.parsers.resolv_conf import ResolvConf
21
22from cloudinit.settings import PER_INSTANCE20from cloudinit.settings import PER_INSTANCE
2321
24LOG = logging.getLogger(__name__)22LOG = logging.getLogger(__name__)
@@ -29,9 +27,8 @@ class Distro(distros.Distro):
29 rc_conf_fn = "/etc/rc.conf"27 rc_conf_fn = "/etc/rc.conf"
30 login_conf_fn = '/etc/login.conf'28 login_conf_fn = '/etc/login.conf'
31 login_conf_fn_bak = '/etc/login.conf.orig'29 login_conf_fn_bak = '/etc/login.conf.orig'
32 resolv_conf_fn = '/etc/resolv.conf'
33 ci_sudoers_fn = '/usr/local/etc/sudoers.d/90-cloud-init-users'30 ci_sudoers_fn = '/usr/local/etc/sudoers.d/90-cloud-init-users'
34 default_primary_nic = 'hn0'31 hostname_conf_fn = '/etc/rc.conf'
3532
36 def __init__(self, name, cfg, paths):33 def __init__(self, name, cfg, paths):
37 distros.Distro.__init__(self, name, cfg, paths)34 distros.Distro.__init__(self, name, cfg, paths)
@@ -40,99 +37,8 @@ class Distro(distros.Distro):
40 # should only happen say once per instance...)37 # should only happen say once per instance...)
41 self._runner = helpers.Runners(paths)38 self._runner = helpers.Runners(paths)
42 self.osfamily = 'freebsd'39 self.osfamily = 'freebsd'
43 self.ipv4_pat = re.compile(r"\s+inet\s+\d+[.]\d+[.]\d+[.]\d+")
44 cfg['ssh_svcname'] = 'sshd'40 cfg['ssh_svcname'] = 'sshd'
4541
46 # Updates a key in /etc/rc.conf.
47 def updatercconf(self, key, value):
48 LOG.debug("Checking %s for: %s = %s", self.rc_conf_fn, key, value)
49 conf = self.loadrcconf()
50 config_changed = False
51 if key not in conf:
52 LOG.debug("Adding key in %s: %s = %s", self.rc_conf_fn, key,
53 value)
54 conf[key] = value
55 config_changed = True
56 else:
57 for item in conf.keys():
58 if item == key and conf[item] != value:
59 conf[item] = value
60 LOG.debug("Changing key in %s: %s = %s", self.rc_conf_fn,
61 key, value)
62 config_changed = True
63
64 if config_changed:
65 LOG.info("Writing %s", self.rc_conf_fn)
66 buf = StringIO()
67 for keyval in conf.items():
68 buf.write('%s="%s"\n' % keyval)
69 util.write_file(self.rc_conf_fn, buf.getvalue())
70
71 # Load the contents of /etc/rc.conf and store all keys in a dict. Make sure
72 # quotes are ignored:
73 # hostname="bla"
74 def loadrcconf(self):
75 RE_MATCH = re.compile(r'^(\w+)\s*=\s*(.*)\s*')
76 conf = {}
77 lines = util.load_file(self.rc_conf_fn).splitlines()
78 for line in lines:
79 m = RE_MATCH.match(line)
80 if not m:
81 LOG.debug("Skipping line from /etc/rc.conf: %s", line)
82 continue
83 key = m.group(1).rstrip()
84 val = m.group(2).rstrip()
85 # Kill them quotes (not completely correct, aka won't handle
86 # quoted values, but should be ok ...)
87 if val[0] in ('"', "'"):
88 val = val[1:]
89 if val[-1] in ('"', "'"):
90 val = val[0:-1]
91 if len(val) == 0:
92 LOG.debug("Skipping empty value from /etc/rc.conf: %s", line)
93 continue
94 conf[key] = val
95 return conf
96
97 def readrcconf(self, key):
98 conf = self.loadrcconf()
99 try:
100 val = conf[key]
101 except KeyError:
102 val = None
103 return val
104
105 # NOVA will inject something like eth0, rewrite that to use the FreeBSD
106 # adapter. Since this adapter is based on the used driver, we need to
107 # figure out which interfaces are available. On KVM platforms this is
108 # vtnet0, where Xen would use xn0.
109 def getnetifname(self, dev):
110 LOG.debug("Translating network interface %s", dev)
111 if dev.startswith('lo'):
112 return dev
113
114 n = re.search(r'\d+$', dev)
115 index = n.group(0)
116
117 (out, _err) = util.subp(['ifconfig', '-a'])
118 ifconfigoutput = [x for x in (out.strip()).splitlines()
119 if len(x.split()) > 0]
120 bsddev = 'NOT_FOUND'
121 for line in ifconfigoutput:
122 m = re.match(r'^\w+', line)
123 if m:
124 if m.group(0).startswith('lo'):
125 continue
126 # Just settle with the first non-lo adapter we find, since it's
127 # rather unlikely there will be multiple nicdrivers involved.
128 bsddev = m.group(0)
129 break
130
131 # Replace the index with the one we're after.
132 bsddev = re.sub(r'\d+$', index, bsddev)
133 LOG.debug("Using network interface %s", bsddev)
134 return bsddev
135
136 def _select_hostname(self, hostname, fqdn):42 def _select_hostname(self, hostname, fqdn):
137 # Should be FQDN if available. See rc.conf(5) in FreeBSD43 # Should be FQDN if available. See rc.conf(5) in FreeBSD
138 if fqdn:44 if fqdn:
@@ -140,21 +46,18 @@ class Distro(distros.Distro):
140 return hostname46 return hostname
14147
142 def _read_system_hostname(self):48 def _read_system_hostname(self):
143 sys_hostname = self._read_hostname(filename=None)49 sys_hostname = self._read_hostname(self.hostname_conf_fn)
144 return ('rc.conf', sys_hostname)50 return (self.hostname_conf_fn, sys_hostname)
14551
146 def _read_hostname(self, filename, default=None):52 def _read_hostname(self, filename, default=None):
147 hostname = None53 (_exists, contents) = rhel_util.read_sysconfig_file(filename)
148 try:54 if contents.get('hostname'):
149 hostname = self.readrcconf('hostname')55 return contents['hostname']
150 except IOError:56 else:
151 pass
152 if not hostname:
153 return default57 return default
154 return hostname
15558
156 def _write_hostname(self, hostname, filename):59 def _write_hostname(self, hostname, filename):
157 self.updatercconf('hostname', hostname)60 rhel_util.update_sysconfig_file(filename, {'hostname': hostname})
15861
159 def create_group(self, name, members):62 def create_group(self, name, members):
160 group_add_cmd = ['pw', '-n', name]63 group_add_cmd = ['pw', '-n', name]
@@ -275,309 +178,16 @@ class Distro(distros.Distro):
275 keys = set(kwargs['ssh_authorized_keys']) or []178 keys = set(kwargs['ssh_authorized_keys']) or []
276 ssh_util.setup_user_keys(keys, name, options=None)179 ssh_util.setup_user_keys(keys, name, options=None)
277180
278 @staticmethod
279 def get_ifconfig_list():
280 cmd = ['ifconfig', '-l']
281 (nics, err) = util.subp(cmd, rcs=[0, 1])
282 if len(err):
283 LOG.warning("Error running %s: %s", cmd, err)
284 return None
285 return nics
286
287 @staticmethod
288 def get_ifconfig_ifname_out(ifname):
289 cmd = ['ifconfig', ifname]
290 (if_result, err) = util.subp(cmd, rcs=[0, 1])
291 if len(err):
292 LOG.warning("Error running %s: %s", cmd, err)
293 return None
294 return if_result
295
296 @staticmethod
297 def get_ifconfig_ether():
298 cmd = ['ifconfig', '-l', 'ether']
299 (nics, err) = util.subp(cmd, rcs=[0, 1])
300 if len(err):
301 LOG.warning("Error running %s: %s", cmd, err)
302 return None
303 return nics
304
305 @staticmethod
306 def get_interface_mac(ifname):
307 if_result = Distro.get_ifconfig_ifname_out(ifname)
308 for item in if_result.splitlines():
309 if item.find('ether ') != -1:
310 mac = str(item.split()[1])
311 if mac:
312 return mac
313
314 @staticmethod
315 def get_devicelist():
316 nics = Distro.get_ifconfig_list()
317 return nics.split()
318
319 @staticmethod
320 def get_ipv6():
321 ipv6 = []
322 nics = Distro.get_devicelist()
323 for nic in nics:
324 if_result = Distro.get_ifconfig_ifname_out(nic)
325 for item in if_result.splitlines():
326 if item.find("inet6 ") != -1 and item.find("scopeid") == -1:
327 ipv6.append(nic)
328 return ipv6
329
330 def get_ipv4(self):
331 ipv4 = []
332 nics = Distro.get_devicelist()
333 for nic in nics:
334 if_result = Distro.get_ifconfig_ifname_out(nic)
335 for item in if_result.splitlines():
336 print(item)
337 if self.ipv4_pat.match(item):
338 ipv4.append(nic)
339 return ipv4
340
341 def is_up(self, ifname):
342 if_result = Distro.get_ifconfig_ifname_out(ifname)
343 pat = "^" + ifname
344 for item in if_result.splitlines():
345 if re.match(pat, item):
346 flags = item.split('<')[1].split('>')[0]
347 if flags.find("UP") != -1:
348 return True
349
350 def _get_current_rename_info(self, check_downable=True):
351 """Collect information necessary for rename_interfaces."""
352 names = Distro.get_devicelist()
353 bymac = {}
354 for n in names:
355 bymac[Distro.get_interface_mac(n)] = {
356 'name': n, 'up': self.is_up(n), 'downable': None}
357
358 nics_with_addresses = set()
359 if check_downable:
360 nics_with_addresses = set(self.get_ipv4() + self.get_ipv6())
361
362 for d in bymac.values():
363 d['downable'] = (d['up'] is False or
364 d['name'] not in nics_with_addresses)
365
366 return bymac
367
368 def _rename_interfaces(self, renames):
369 if not len(renames):
370 LOG.debug("no interfaces to rename")
371 return
372
373 current_info = self._get_current_rename_info()
374
375 cur_bymac = {}
376 for mac, data in current_info.items():
377 cur = data.copy()
378 cur['mac'] = mac
379 cur_bymac[mac] = cur
380
381 def update_byname(bymac):
382 return dict((data['name'], data)
383 for data in bymac.values())
384
385 def rename(cur, new):
386 util.subp(["ifconfig", cur, "name", new], capture=True)
387
388 def down(name):
389 util.subp(["ifconfig", name, "down"], capture=True)
390
391 def up(name):
392 util.subp(["ifconfig", name, "up"], capture=True)
393
394 ops = []
395 errors = []
396 ups = []
397 cur_byname = update_byname(cur_bymac)
398 tmpname_fmt = "cirename%d"
399 tmpi = -1
400
401 for mac, new_name in renames:
402 cur = cur_bymac.get(mac, {})
403 cur_name = cur.get('name')
404 cur_ops = []
405 if cur_name == new_name:
406 # nothing to do
407 continue
408
409 if not cur_name:
410 errors.append("[nic not present] Cannot rename mac=%s to %s"
411 ", not available." % (mac, new_name))
412 continue
413
414 if cur['up']:
415 msg = "[busy] Error renaming mac=%s from %s to %s"
416 if not cur['downable']:
417 errors.append(msg % (mac, cur_name, new_name))
418 continue
419 cur['up'] = False
420 cur_ops.append(("down", mac, new_name, (cur_name,)))
421 ups.append(("up", mac, new_name, (new_name,)))
422
423 if new_name in cur_byname:
424 target = cur_byname[new_name]
425 if target['up']:
426 msg = "[busy-target] Error renaming mac=%s from %s to %s."
427 if not target['downable']:
428 errors.append(msg % (mac, cur_name, new_name))
429 continue
430 else:
431 cur_ops.append(("down", mac, new_name, (new_name,)))
432
433 tmp_name = None
434 while tmp_name is None or tmp_name in cur_byname:
435 tmpi += 1
436 tmp_name = tmpname_fmt % tmpi
437
438 cur_ops.append(("rename", mac, new_name, (new_name, tmp_name)))
439 target['name'] = tmp_name
440 cur_byname = update_byname(cur_bymac)
441 if target['up']:
442 ups.append(("up", mac, new_name, (tmp_name,)))
443
444 cur_ops.append(("rename", mac, new_name, (cur['name'], new_name)))
445 cur['name'] = new_name
446 cur_byname = update_byname(cur_bymac)
447 ops += cur_ops
448
449 opmap = {'rename': rename, 'down': down, 'up': up}
450 if len(ops) + len(ups) == 0:
451 if len(errors):
452 LOG.debug("unable to do any work for renaming of %s", renames)
453 else:
454 LOG.debug("no work necessary for renaming of %s", renames)
455 else:
456 LOG.debug("achieving renaming of %s with ops %s",
457 renames, ops + ups)
458
459 for op, mac, new_name, params in ops + ups:
460 try:
461 opmap.get(op)(*params)
462 except Exception as e:
463 errors.append(
464 "[unknown] Error performing %s%s for %s, %s: %s" %
465 (op, params, mac, new_name, e))
466 if len(errors):
467 raise Exception('\n'.join(errors))
468
469 def apply_network_config_names(self, netcfg):
470 renames = []
471 for ent in netcfg.get('config', {}):
472 if ent.get('type') != 'physical':
473 continue
474 mac = ent.get('mac_address')
475 name = ent.get('name')
476 if not mac:
477 continue
478 renames.append([mac, name])
479 return self._rename_interfaces(renames)
480
481 @classmethod
482 def generate_fallback_config(self):181 def generate_fallback_config(self):
483 nics = Distro.get_ifconfig_ether()182 nconf = {'config': [], 'version': 1}
484 if nics is None:183 for mac, name in net.get_interfaces_by_mac().items():
485 LOG.debug("Fail to get network interfaces")
486 return None
487 potential_interfaces = nics.split()
488 connected = []
489 for nic in potential_interfaces:
490 pat = "^" + nic
491 if_result = Distro.get_ifconfig_ifname_out(nic)
492 for item in if_result.split("\n"):
493 if re.match(pat, item):
494 flags = item.split('<')[1].split('>')[0]
495 if flags.find("RUNNING") != -1:
496 connected.append(nic)
497 if connected:
498 potential_interfaces = connected
499 names = list(sorted(potential_interfaces))
500 default_pri_nic = Distro.default_primary_nic
501 if default_pri_nic in names:
502 names.remove(default_pri_nic)
503 names.insert(0, default_pri_nic)
504 target_name = None
505 target_mac = None
506 for name in names:
507 mac = Distro.get_interface_mac(name)
508 if mac:
509 target_name = name
510 target_mac = mac
511 break
512 if target_mac and target_name:
513 nconf = {'config': [], 'version': 1}
514 nconf['config'].append(184 nconf['config'].append(
515 {'type': 'physical', 'name': target_name,185 {'type': 'physical', 'name': name,
516 'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]})186 'mac_address': mac, 'subnets': [{'type': 'dhcp'}]})
517 return nconf187 return nconf
518 else:
519 return None
520
521 def _write_network(self, settings):
522 entries = net_util.translate_network(settings)
523 nameservers = []
524 searchdomains = []
525 dev_names = entries.keys()
526 for (device, info) in entries.items():
527 # Skip the loopback interface.
528 if device.startswith('lo'):
529 continue
530
531 dev = self.getnetifname(device)
532
533 LOG.info('Configuring interface %s', dev)
534
535 if info.get('bootproto') == 'static':
536 LOG.debug('Configuring dev %s with %s / %s', dev,
537 info.get('address'), info.get('netmask'))
538 # Configure an ipv4 address.
539 ifconfig = (info.get('address') + ' netmask ' +
540 info.get('netmask'))
541
542 # Configure the gateway.
543 self.updatercconf('defaultrouter', info.get('gateway'))
544
545 if 'dns-nameservers' in info:
546 nameservers.extend(info['dns-nameservers'])
547 if 'dns-search' in info:
548 searchdomains.extend(info['dns-search'])
549 else:
550 ifconfig = 'DHCP'
551
552 self.updatercconf('ifconfig_' + dev, ifconfig)
553
554 # Try to read the /etc/resolv.conf or just start from scratch if that
555 # fails.
556 try:
557 resolvconf = ResolvConf(util.load_file(self.resolv_conf_fn))
558 resolvconf.parse()
559 except IOError:
560 util.logexc(LOG, "Failed to parse %s, use new empty file",
561 self.resolv_conf_fn)
562 resolvconf = ResolvConf('')
563 resolvconf.parse()
564
565 # Add some nameservers
566 for server in nameservers:
567 try:
568 resolvconf.add_nameserver(server)
569 except ValueError:
570 util.logexc(LOG, "Failed to add nameserver %s", server)
571
572 # And add any searchdomains.
573 for domain in searchdomains:
574 try:
575 resolvconf.add_search_domain(domain)
576 except ValueError:
577 util.logexc(LOG, "Failed to add search domain %s", domain)
578 util.write_file(self.resolv_conf_fn, str(resolvconf), 0o644)
579188
580 return dev_names189 def _write_network_config(self, netconfig):
190 return self._supported_write_network_config(netconfig)
581191
582 def apply_locale(self, locale, out_fn=None):192 def apply_locale(self, locale, out_fn=None):
583 # Adjust the locals value to the new value193 # Adjust the locals value to the new value
@@ -605,18 +215,12 @@ class Distro(distros.Distro):
605 util.logexc(LOG, "Failed to restore %s backup",215 util.logexc(LOG, "Failed to restore %s backup",
606 self.login_conf_fn)216 self.login_conf_fn)
607217
608 def _bring_up_interface(self, device_name):218 def apply_network_config_names(self, netconfig):
609 if device_name.startswith('lo'):219 # This is handled by the freebsd network renderer. It writes in
610 return220 # /etc/rc.conf a line with the following format:
611 dev = self.getnetifname(device_name)221 # ifconfig_OLDNAME_name=NEWNAME
612 cmd = ['/etc/rc.d/netif', 'start', dev]222 # FreeBSD network script will rename the interface automatically.
613 LOG.debug("Attempting to bring up interface %s using command %s",223 return
614 dev, cmd)
615 # This could return 1 when the interface has already been put UP by the
616 # OS. This is just fine.
617 (_out, err) = util.subp(cmd, rcs=[0, 1])
618 if len(err):
619 LOG.warning("Error running %s: %s", cmd, err)
620224
621 def install_packages(self, pkglist):225 def install_packages(self, pkglist):
622 self.update_package_sources()226 self.update_package_sources()
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index bd80637..7048f14 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -765,6 +765,36 @@ def get_ib_interface_hwaddr(ifname, ethernet_format):
765765
766766
767def get_interfaces_by_mac():767def get_interfaces_by_mac():
768 if util.is_FreeBSD():
769 return get_interfaces_by_mac_on_freebsd()
770 else:
771 return get_interfaces_by_mac_on_linux()
772
773
774def get_interfaces_by_mac_on_freebsd():
775 (out, _) = util.subp(['ifconfig', '-a', 'ether'])
776 # flatten each interface block in a single line
777 def flatten(out):
778 curr_block = ''
779 for l in out.split('\n'):
780 if l.startswith(' '):
781 curr_block += l
782 else:
783 if curr_block:
784 yield curr_block
785 curr_block = l
786 yield curr_block
787 # looks for interface and mac in a list of flatten block
788 def find_mac(flat_list):
789 for block in flat_list:
790 m = re.search(r"^(\S*): .*ether\s([\da-f:]{17}).*", block)
791 if m:
792 yield (m.group(1), m.group(2))
793 results = {i[0]: i[1] for i in find_mac(flatten(out))}
794 return results
795
796
797def get_interfaces_by_mac_on_linux():
768 """Build a dictionary of tuples {mac: name}.798 """Build a dictionary of tuples {mac: name}.
769799
770 Bridges and any devices that have a 'stolen' mac are excluded."""800 Bridges and any devices that have a 'stolen' mac are excluded."""
diff --git a/cloudinit/net/freebsd.py b/cloudinit/net/freebsd.py
771new file mode 100644801new file mode 100644
index 0000000..066d5eb
--- /dev/null
+++ b/cloudinit/net/freebsd.py
@@ -0,0 +1,123 @@
1# This file is part of cloud-init. See LICENSE file for license information.
2
3import re
4
5from cloudinit import log as logging
6from cloudinit import net
7from cloudinit import util
8from cloudinit.distros import rhel_util
9from cloudinit.distros.parsers.resolv_conf import ResolvConf
10
11from . import renderer
12
13LOG = logging.getLogger(__name__)
14
15
16class Renderer(renderer.Renderer):
17 resolv_conf_fn = 'etc/resolv.conf'
18 rc_conf_fn = 'etc/rc.conf'
19
20 def __init__(self, config=None):
21 if not config:
22 config = {}
23 self.dhcp_interfaces = []
24 self._postcmds = config.get('postcmds', True)
25
26 def _render_route(self, route, indent=""):
27 pass
28
29 def _render_iface(self, iface, render_hwaddress=False):
30 pass
31
32 def _write_network(self, settings, target=None):
33 nameservers = []
34 searchdomains = []
35 ifname_by_mac = net.get_interfaces_by_mac()
36 for interface in settings.iter_interfaces():
37 device_name = interface.get("name")
38 device_mac = interface.get("mac_address")
39 if device_name and re.match(r'^lo\d+$', device_name):
40 continue
41 if device_mac and device_name:
42 cur_name = ifname_by_mac[device_mac]
43 if not cur_name:
44 LOG.info('Cannot find any device with MAC %s', device_mac)
45 continue
46 if cur_name != device_name:
47 rhel_util.update_sysconfig_file(
48 util.target_path(target, self.rc_conf_fn), {
49 'ifconfig_%s_name' % cur_name: device_name})
50 elif device_mac:
51 device_name = ifname_by_mac[device_mac]
52
53 subnet = interface.get("subnets", [])[0]
54 LOG.info('Configuring interface %s', device_name)
55
56 if subnet.get('type') == 'static':
57 LOG.debug('Configuring dev %s with %s / %s', device_name,
58 subnet.get('address'), subnet.get('netmask'))
59 # Configure an ipv4 address.
60 ifconfig = (subnet.get('address') + ' netmask ' +
61 subnet.get('netmask'))
62
63 # Configure the gateway.
64 rhel_util.update_sysconfig_file(
65 util.target_path(target, self.rc_conf_fn), {
66 'defaultrouter': subnet.get('gateway')})
67
68 if 'dns_nameservers' in subnet:
69 nameservers.extend(subnet['dns_nameservers'])
70 if 'dns_search' in subnet:
71 searchdomains.extend(subnet['dns_search'])
72 else:
73 self.dhcp_interfaces.append(device_name)
74 ifconfig = 'DHCP'
75
76 rhel_util.update_sysconfig_file(
77 util.target_path(target, self.rc_conf_fn), {
78 'ifconfig_' + device_name: ifconfig})
79
80 # Try to read the /etc/resolv.conf or just start from scratch if that
81 # fails.
82 try:
83 resolvconf = ResolvConf(util.load_file(self.resolv_conf_fn))
84 resolvconf.parse()
85 except IOError:
86 util.logexc(LOG, "Failed to parse %s, use new empty file",
87 self.resolv_conf_fn)
88 resolvconf = ResolvConf('')
89 resolvconf.parse()
90
91 # Add some nameservers
92 for server in nameservers:
93 try:
94 resolvconf.add_nameserver(server)
95 except ValueError:
96 util.logexc(LOG, "Failed to add nameserver %s", server)
97
98 # And add any searchdomains.
99 for domain in searchdomains:
100 try:
101 resolvconf.add_search_domain(domain)
102 except ValueError:
103 util.logexc(LOG, "Failed to add search domain %s", domain)
104 util.write_file(self.resolv_conf_fn, str(resolvconf), 0o644)
105 self.start_services()
106
107 def render_network_state(self, network_state, templates=None, target=None):
108 self._write_network(network_state, target=target)
109
110 def start_services(self):
111 if not self._postcmds:
112 LOG.debug("freebsd generate postcmd disabled")
113 return
114
115 util.subp(['service', 'netif', 'restart'], capture=True)
116 util.subp(['service', 'routing', 'restart'], capture=True)
117 for dhcp_interface in self.dhcp_interfaces:
118 util.subp(['service', 'dhclient', 'restart', dhcp_interface],
119 capture=True)
120
121
122def available(target=None):
123 return util.is_FreeBSD()
diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py
index 5117b4a..b98dbbe 100644
--- a/cloudinit/net/renderers.py
+++ b/cloudinit/net/renderers.py
@@ -1,17 +1,19 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3from . import eni3from . import eni
4from . import freebsd
4from . import netplan5from . import netplan
5from . import RendererNotFoundError6from . import RendererNotFoundError
6from . import sysconfig7from . import sysconfig
78
8NAME_TO_RENDERER = {9NAME_TO_RENDERER = {
9 "eni": eni,10 "eni": eni,
11 "freebsd": freebsd,
10 "netplan": netplan,12 "netplan": netplan,
11 "sysconfig": sysconfig,13 "sysconfig": sysconfig,
12}14}
1315
14DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan"]16DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan", "freebsd"]
1517
1618
17def search(priority=None, target=None, first=False):19def search(priority=None, target=None, first=False):
diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst
index 51ced4d..1520ba9 100644
--- a/doc/rtd/topics/network-config.rst
+++ b/doc/rtd/topics/network-config.rst
@@ -191,7 +191,7 @@ supplying an updated configuration in cloud-config. ::
191191
192 system_info:192 system_info:
193 network:193 network:
194 renderers: ['netplan', 'eni', 'sysconfig']194 renderers: ['netplan', 'eni', 'sysconfig', 'freebsd']
195195
196196
197Network Configuration Tools197Network Configuration Tools
diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
index 6720995..4c14d8c 100644
--- a/tests/unittests/test_distros/test_netconfig.py
+++ b/tests/unittests/test_distros/test_netconfig.py
@@ -1,5 +1,6 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3import copy
3import os4import os
4from six import StringIO5from six import StringIO
5from textwrap import dedent6from textwrap import dedent
@@ -14,7 +15,7 @@ from cloudinit.distros.parsers.sys_conf import SysConf
14from cloudinit import helpers15from cloudinit import helpers
15from cloudinit import settings16from cloudinit import settings
16from cloudinit.tests.helpers import (17from cloudinit.tests.helpers import (
17 FilesystemMockingTestCase, dir2dict, populate_dir)18 FilesystemMockingTestCase, dir2dict)
18from cloudinit import util19from cloudinit import util
1920
2021
@@ -213,128 +214,95 @@ class TestNetCfgDistroBase(FilesystemMockingTestCase):
213 self.assertEqual(v, b2[k])214 self.assertEqual(v, b2[k])
214215
215216
216class TestNetCfgDistroFreebsd(TestNetCfgDistroBase):217class TestNetCfgDistroFreeBSD(TestNetCfgDistroBase):
217218
218 frbsd_ifout = """\219 def setUp(self):
219hn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500220 super(TestNetCfgDistroFreeBSD, self).setUp()
220 options=51b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,TSO4,LRO>221 self.distro = self._get_distro('freebsd', renderers=['freebsd'])
221 ether 00:15:5d:4c:73:00222
222 inet6 fe80::215:5dff:fe4c:7300%hn0 prefixlen 64 scopeid 0x2223 def _apply_and_verify_freebsd(self, apply_fn, config, expected_cfgs=None,
223 inet 10.156.76.127 netmask 0xfffffc00 broadcast 10.156.79.255224 bringup=False):
224 nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL>225 if not expected_cfgs:
225 media: Ethernet autoselect (10Gbase-T <full-duplex>)226 raise ValueError('expected_cfg must not be None')
226 status: active227
228 tmpd = None
229 with mock.patch('cloudinit.net.freebsd.available') as m_avail:
230 m_avail.return_value = True
231 with self.reRooted(tmpd) as tmpd:
232 util.ensure_dir('/etc')
233 util.ensure_file('/etc/rc.conf')
234 util.ensure_file('/etc/resolv.conf')
235 apply_fn(config, bringup)
236
237 results = dir2dict(tmpd)
238 for cfgpath, expected in expected_cfgs.items():
239 print("----------")
240 print(expected)
241 print("^^^^ expected | rendered VVVVVVV")
242 print(results[cfgpath])
243 print("----------")
244 self.assertEqual(
245 set(expected.split('\n')),
246 set(results[cfgpath].split('\n')))
247 self.assertEqual(0o644, get_mode(cfgpath, tmpd))
248
249 @mock.patch('cloudinit.net.get_interfaces_by_mac')
250 def test_apply_network_config_freebsd_standard(self, ifaces_mac):
251 ifaces_mac.return_value = {
252 '00:15:5d:4c:73:00': 'eth0',
253 }
254 rc_conf_expected = """\
255defaultrouter=192.168.1.254
256ifconfig_eth0='192.168.1.5 netmask 255.255.255.0'
257ifconfig_eth1=DHCP
227"""258"""
228259
229 @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_list')260 expected_cfgs = {
230 @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ifname_out')261 '/etc/rc.conf': rc_conf_expected,
231 def test_get_ip_nic_freebsd(self, ifname_out, iflist):262 '/etc/resolv.conf': ''
232 frbsd_distro = self._get_distro('freebsd')263 }
233 iflist.return_value = "lo0 hn0"264 self._apply_and_verify_freebsd(self.distro.apply_network_config,
234 ifname_out.return_value = self.frbsd_ifout265 V1_NET_CFG,
235 res = frbsd_distro.get_ipv4()266 expected_cfgs=expected_cfgs.copy())
236 self.assertEqual(res, ['lo0', 'hn0'])267
237 res = frbsd_distro.get_ipv6()268 @mock.patch('cloudinit.net.get_interfaces_by_mac')
238 self.assertEqual(res, [])269 def test_apply_network_config_freebsd_ifrename(self, ifaces_mac):
239270 ifaces_mac.return_value = {
240 @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ether')271 '00:15:5d:4c:73:00': 'vtnet0',
241 @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ifname_out')
242 @mock.patch('cloudinit.distros.freebsd.Distro.get_interface_mac')
243 def test_generate_fallback_config_freebsd(self, mac, ifname_out, if_ether):
244 frbsd_distro = self._get_distro('freebsd')
245
246 if_ether.return_value = 'hn0'
247 ifname_out.return_value = self.frbsd_ifout
248 mac.return_value = '00:15:5d:4c:73:00'
249 res = frbsd_distro.generate_fallback_config()
250 self.assertIsNotNone(res)
251
252 def test_simple_write_freebsd(self):
253 fbsd_distro = self._get_distro('freebsd')
254
255 rc_conf = '/etc/rc.conf'
256 read_bufs = {
257 rc_conf: 'initial-rc-conf-not-validated',
258 '/etc/resolv.conf': 'initial-resolv-conf-not-validated',
259 }272 }
273 rc_conf_expected = """\
274ifconfig_vtnet0_name=eth0
275defaultrouter=192.168.1.254
276ifconfig_eth0='192.168.1.5 netmask 255.255.255.0'
277ifconfig_eth1=DHCP
278"""
260279
261 tmpd = self.tmp_dir()280 V1_NET_CFG_RENAME = copy.deepcopy(V1_NET_CFG)
262 populate_dir(tmpd, read_bufs)281 V1_NET_CFG_RENAME['config'][0]['mac_address'] = '00:15:5d:4c:73:00'
263 with self.reRooted(tmpd):282
264 with mock.patch("cloudinit.distros.freebsd.util.subp",283 expected_cfgs = {
265 return_value=('vtnet0', '')):284 '/etc/rc.conf': rc_conf_expected,
266 fbsd_distro.apply_network(BASE_NET_CFG, False)285 '/etc/resolv.conf': ''
267 results = dir2dict(tmpd)
268
269 self.assertIn(rc_conf, results)
270 self.assertCfgEquals(
271 dedent('''\
272 ifconfig_vtnet0="192.168.1.5 netmask 255.255.255.0"
273 ifconfig_vtnet1="DHCP"
274 defaultrouter="192.168.1.254"
275 '''), results[rc_conf])
276 self.assertEqual(0o644, get_mode(rc_conf, tmpd))
277
278 def test_simple_write_freebsd_from_v2eni(self):
279 fbsd_distro = self._get_distro('freebsd')
280
281 rc_conf = '/etc/rc.conf'
282 read_bufs = {
283 rc_conf: 'initial-rc-conf-not-validated',
284 '/etc/resolv.conf': 'initial-resolv-conf-not-validated',
285 }286 }
287 self._apply_and_verify_freebsd(self.distro.apply_network_config,
288 V1_NET_CFG_RENAME,
289 expected_cfgs=expected_cfgs.copy())
286290
287 tmpd = self.tmp_dir()291 @mock.patch('cloudinit.net.get_interfaces_by_mac')
288 populate_dir(tmpd, read_bufs)292 def test_apply_network_config_freebsd_nameserver(self, ifaces_mac):
289 with self.reRooted(tmpd):293 ifaces_mac.return_value = {
290 with mock.patch("cloudinit.distros.freebsd.util.subp",294 '00:15:5d:4c:73:00': 'eth0',
291 return_value=('vtnet0', '')):
292 fbsd_distro.apply_network(BASE_NET_CFG_FROM_V2, False)
293 results = dir2dict(tmpd)
294
295 self.assertIn(rc_conf, results)
296 self.assertCfgEquals(
297 dedent('''\
298 ifconfig_vtnet0="192.168.1.5 netmask 255.255.255.0"
299 ifconfig_vtnet1="DHCP"
300 defaultrouter="192.168.1.254"
301 '''), results[rc_conf])
302 self.assertEqual(0o644, get_mode(rc_conf, tmpd))
303
304 def test_apply_network_config_fallback_freebsd(self):
305 fbsd_distro = self._get_distro('freebsd')
306
307 # a weak attempt to verify that we don't have an implementation
308 # of _write_network_config or apply_network_config in fbsd now,
309 # which would make this test not actually test the fallback.
310 self.assertRaises(
311 NotImplementedError, fbsd_distro._write_network_config,
312 BASE_NET_CFG)
313
314 # now run
315 mynetcfg = {
316 'config': [{"type": "physical", "name": "eth0",
317 "mac_address": "c0:d6:9f:2c:e8:80",
318 "subnets": [{"type": "dhcp"}]}],
319 'version': 1}
320
321 rc_conf = '/etc/rc.conf'
322 read_bufs = {
323 rc_conf: 'initial-rc-conf-not-validated',
324 '/etc/resolv.conf': 'initial-resolv-conf-not-validated',
325 }295 }
326296
327 tmpd = self.tmp_dir()297 V1_NET_CFG_DNS = copy.deepcopy(V1_NET_CFG)
328 populate_dir(tmpd, read_bufs)298 ns = ['1.2.3.4']
329 with self.reRooted(tmpd):299 V1_NET_CFG_DNS['config'][0]['subnets'][0]['dns_nameservers'] = ns
330 with mock.patch("cloudinit.distros.freebsd.util.subp",300 expected_cfgs = {
331 return_value=('vtnet0', '')):301 '/etc/resolv.conf': 'nameserver 1.2.3.4\n'
332 fbsd_distro.apply_network_config(mynetcfg, bring_up=False)302 }
333 results = dir2dict(tmpd)303 self._apply_and_verify_freebsd(self.distro.apply_network_config,
334304 V1_NET_CFG_DNS,
335 self.assertIn(rc_conf, results)305 expected_cfgs=expected_cfgs.copy())
336 self.assertCfgEquals('ifconfig_vtnet0="DHCP"', results[rc_conf])
337 self.assertEqual(0o644, get_mode(rc_conf, tmpd))
338306
339307
340class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase):308class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase):
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 0f45dc3..47a50e7 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -2409,7 +2409,6 @@ DEFAULT_DEV_ATTRS = {
2409 }2409 }
2410}2410}
24112411
2412
2413def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net,2412def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net,
2414 mock_sys_dev_path, dev_attrs=None):2413 mock_sys_dev_path, dev_attrs=None):
2415 if not dev_attrs:2414 if not dev_attrs:
diff --git a/tests/unittests/test_net_freebsd.py b/tests/unittests/test_net_freebsd.py
2416new file mode 1006442415new file mode 100644
index 0000000..8adb14e
--- /dev/null
+++ b/tests/unittests/test_net_freebsd.py
@@ -0,0 +1,29 @@
1from cloudinit import net
2
3from cloudinit.tests.helpers import (CiTestCase, mock)
4
5
6IFCONFIG_FREEBSD = """vtnet0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
7 options=6c07bb<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,JUMBO_MTU,VLAN_HWCSUM,TSO4,TSO6,LRO,VLAN_HWTSO,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
8 ether 52:54:00:50:b7:0d
9re0.33: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> metric 0 mtu 1500
10 options=80003<RXCSUM,TXCSUM,LINKSTATE>
11 ether 80:00:73:63:5c:48
12 groups: vlan
13 vlan: 33 vlanpcp: 0 parent interface: re0
14 media: Ethernet autoselect (1000baseT <full-duplex,master>)
15 status: active
16 nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
17lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
18 options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
19"""
20
21class TestInterfacesByMac(CiTestCase):
22
23 @mock.patch('cloudinit.util.subp')
24 @mock.patch('cloudinit.util.is_FreeBSD')
25 def test_get_interfaces_by_mac(self, mock_is_FreeBSD, mock_subp):
26 mock_is_FreeBSD.return_value = True
27 mock_subp.return_value = (IFCONFIG_FREEBSD, 0)
28 a = net.get_interfaces_by_mac()
29 assert a == {"vtnet0": "52:54:00:50:b7:0d", "re0.33": "80:00:73:63:5c:48"}

Subscribers

People subscribed via source and target branches