Merge ~goneri/cloud-init:freebsd_net_renderer into cloud-init:master
- Git
- lp:~goneri/cloud-init
- freebsd_net_renderer
- Merge into master
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) |
Related bugs: |
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_
access to /etc/rc.conf.
Interfaces are not automatically renamed by FreeBSD using
the following configuration in /etc/rc.conf:
```
ifconfig_
```
Description of the change
Gonéri Le Bouder (goneri) wrote : | # |
I pushed an update to address your comments.
Ryan Harper (raharper) wrote : | # |
Thanks. I've added a few more questions/comments inline.
Gonéri Le Bouder (goneri) wrote : | # |
Thanks Ryan for the review.
Gonéri Le Bouder (goneri) : | # |
Ryan Harper (raharper) : | # |
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:/
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.
Gonéri Le Bouder (goneri) wrote : | # |
Ryan, I think I've addressed all the comments.
Ryan Harper (raharper) wrote : | # |
Thanks for the update. I've added some additional comments inline. Thanks for continue to work on this.
Andrey Fesenko (f0andrey) wrote : | # |
Thanks it's fix first start network.
I'm submit update in port FreeBSD
https:/
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:/
https:/
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:/
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:/
https:/
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:/
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:/
https:/
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:/
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:/
https:/
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:/
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:/
https:/
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:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:fb42c3a3a00
https:/
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:/
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.
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:/
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:0f6af36387d
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
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
1 | diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py | |||
2 | index 00bdee3..75bb441 100644 | |||
3 | --- a/cloudinit/distros/__init__.py | |||
4 | +++ b/cloudinit/distros/__init__.py | |||
5 | @@ -145,7 +145,7 @@ class Distro(object): | |||
6 | 145 | # Write it out | 145 | # Write it out |
7 | 146 | 146 | ||
8 | 147 | # pylint: disable=assignment-from-no-return | 147 | # pylint: disable=assignment-from-no-return |
10 | 148 | # We have implementations in arch, freebsd and gentoo still | 148 | # We have implementations in arch and gentoo still |
11 | 149 | dev_names = self._write_network(settings) | 149 | dev_names = self._write_network(settings) |
12 | 150 | # pylint: enable=assignment-from-no-return | 150 | # pylint: enable=assignment-from-no-return |
13 | 151 | # Now try to bring them up | 151 | # Now try to bring them up |
14 | diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py | |||
15 | index c55f899..3224887 100644 | |||
16 | --- a/cloudinit/distros/freebsd.py | |||
17 | +++ b/cloudinit/distros/freebsd.py | |||
18 | @@ -13,12 +13,10 @@ import re | |||
19 | 13 | from cloudinit import distros | 13 | from cloudinit import distros |
20 | 14 | from cloudinit import helpers | 14 | from cloudinit import helpers |
21 | 15 | from cloudinit import log as logging | 15 | from cloudinit import log as logging |
22 | 16 | from cloudinit import net | ||
23 | 16 | from cloudinit import ssh_util | 17 | from cloudinit import ssh_util |
24 | 17 | from cloudinit import util | 18 | from cloudinit import util |
29 | 18 | 19 | from cloudinit.distros import rhel_util | |
26 | 19 | from cloudinit.distros import net_util | ||
27 | 20 | from cloudinit.distros.parsers.resolv_conf import ResolvConf | ||
28 | 21 | |||
30 | 22 | from cloudinit.settings import PER_INSTANCE | 20 | from cloudinit.settings import PER_INSTANCE |
31 | 23 | 21 | ||
32 | 24 | LOG = logging.getLogger(__name__) | 22 | LOG = logging.getLogger(__name__) |
33 | @@ -29,9 +27,8 @@ class Distro(distros.Distro): | |||
34 | 29 | rc_conf_fn = "/etc/rc.conf" | 27 | rc_conf_fn = "/etc/rc.conf" |
35 | 30 | login_conf_fn = '/etc/login.conf' | 28 | login_conf_fn = '/etc/login.conf' |
36 | 31 | login_conf_fn_bak = '/etc/login.conf.orig' | 29 | login_conf_fn_bak = '/etc/login.conf.orig' |
37 | 32 | resolv_conf_fn = '/etc/resolv.conf' | ||
38 | 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' |
40 | 34 | default_primary_nic = 'hn0' | 31 | hostname_conf_fn = '/etc/rc.conf' |
41 | 35 | 32 | ||
42 | 36 | def __init__(self, name, cfg, paths): | 33 | def __init__(self, name, cfg, paths): |
43 | 37 | distros.Distro.__init__(self, name, cfg, paths) | 34 | distros.Distro.__init__(self, name, cfg, paths) |
44 | @@ -40,99 +37,8 @@ class Distro(distros.Distro): | |||
45 | 40 | # should only happen say once per instance...) | 37 | # should only happen say once per instance...) |
46 | 41 | self._runner = helpers.Runners(paths) | 38 | self._runner = helpers.Runners(paths) |
47 | 42 | self.osfamily = 'freebsd' | 39 | self.osfamily = 'freebsd' |
48 | 43 | self.ipv4_pat = re.compile(r"\s+inet\s+\d+[.]\d+[.]\d+[.]\d+") | ||
49 | 44 | cfg['ssh_svcname'] = 'sshd' | 40 | cfg['ssh_svcname'] = 'sshd' |
50 | 45 | 41 | ||
51 | 46 | # Updates a key in /etc/rc.conf. | ||
52 | 47 | def updatercconf(self, key, value): | ||
53 | 48 | LOG.debug("Checking %s for: %s = %s", self.rc_conf_fn, key, value) | ||
54 | 49 | conf = self.loadrcconf() | ||
55 | 50 | config_changed = False | ||
56 | 51 | if key not in conf: | ||
57 | 52 | LOG.debug("Adding key in %s: %s = %s", self.rc_conf_fn, key, | ||
58 | 53 | value) | ||
59 | 54 | conf[key] = value | ||
60 | 55 | config_changed = True | ||
61 | 56 | else: | ||
62 | 57 | for item in conf.keys(): | ||
63 | 58 | if item == key and conf[item] != value: | ||
64 | 59 | conf[item] = value | ||
65 | 60 | LOG.debug("Changing key in %s: %s = %s", self.rc_conf_fn, | ||
66 | 61 | key, value) | ||
67 | 62 | config_changed = True | ||
68 | 63 | |||
69 | 64 | if config_changed: | ||
70 | 65 | LOG.info("Writing %s", self.rc_conf_fn) | ||
71 | 66 | buf = StringIO() | ||
72 | 67 | for keyval in conf.items(): | ||
73 | 68 | buf.write('%s="%s"\n' % keyval) | ||
74 | 69 | util.write_file(self.rc_conf_fn, buf.getvalue()) | ||
75 | 70 | |||
76 | 71 | # Load the contents of /etc/rc.conf and store all keys in a dict. Make sure | ||
77 | 72 | # quotes are ignored: | ||
78 | 73 | # hostname="bla" | ||
79 | 74 | def loadrcconf(self): | ||
80 | 75 | RE_MATCH = re.compile(r'^(\w+)\s*=\s*(.*)\s*') | ||
81 | 76 | conf = {} | ||
82 | 77 | lines = util.load_file(self.rc_conf_fn).splitlines() | ||
83 | 78 | for line in lines: | ||
84 | 79 | m = RE_MATCH.match(line) | ||
85 | 80 | if not m: | ||
86 | 81 | LOG.debug("Skipping line from /etc/rc.conf: %s", line) | ||
87 | 82 | continue | ||
88 | 83 | key = m.group(1).rstrip() | ||
89 | 84 | val = m.group(2).rstrip() | ||
90 | 85 | # Kill them quotes (not completely correct, aka won't handle | ||
91 | 86 | # quoted values, but should be ok ...) | ||
92 | 87 | if val[0] in ('"', "'"): | ||
93 | 88 | val = val[1:] | ||
94 | 89 | if val[-1] in ('"', "'"): | ||
95 | 90 | val = val[0:-1] | ||
96 | 91 | if len(val) == 0: | ||
97 | 92 | LOG.debug("Skipping empty value from /etc/rc.conf: %s", line) | ||
98 | 93 | continue | ||
99 | 94 | conf[key] = val | ||
100 | 95 | return conf | ||
101 | 96 | |||
102 | 97 | def readrcconf(self, key): | ||
103 | 98 | conf = self.loadrcconf() | ||
104 | 99 | try: | ||
105 | 100 | val = conf[key] | ||
106 | 101 | except KeyError: | ||
107 | 102 | val = None | ||
108 | 103 | return val | ||
109 | 104 | |||
110 | 105 | # NOVA will inject something like eth0, rewrite that to use the FreeBSD | ||
111 | 106 | # adapter. Since this adapter is based on the used driver, we need to | ||
112 | 107 | # figure out which interfaces are available. On KVM platforms this is | ||
113 | 108 | # vtnet0, where Xen would use xn0. | ||
114 | 109 | def getnetifname(self, dev): | ||
115 | 110 | LOG.debug("Translating network interface %s", dev) | ||
116 | 111 | if dev.startswith('lo'): | ||
117 | 112 | return dev | ||
118 | 113 | |||
119 | 114 | n = re.search(r'\d+$', dev) | ||
120 | 115 | index = n.group(0) | ||
121 | 116 | |||
122 | 117 | (out, _err) = util.subp(['ifconfig', '-a']) | ||
123 | 118 | ifconfigoutput = [x for x in (out.strip()).splitlines() | ||
124 | 119 | if len(x.split()) > 0] | ||
125 | 120 | bsddev = 'NOT_FOUND' | ||
126 | 121 | for line in ifconfigoutput: | ||
127 | 122 | m = re.match(r'^\w+', line) | ||
128 | 123 | if m: | ||
129 | 124 | if m.group(0).startswith('lo'): | ||
130 | 125 | continue | ||
131 | 126 | # Just settle with the first non-lo adapter we find, since it's | ||
132 | 127 | # rather unlikely there will be multiple nicdrivers involved. | ||
133 | 128 | bsddev = m.group(0) | ||
134 | 129 | break | ||
135 | 130 | |||
136 | 131 | # Replace the index with the one we're after. | ||
137 | 132 | bsddev = re.sub(r'\d+$', index, bsddev) | ||
138 | 133 | LOG.debug("Using network interface %s", bsddev) | ||
139 | 134 | return bsddev | ||
140 | 135 | |||
141 | 136 | def _select_hostname(self, hostname, fqdn): | 42 | def _select_hostname(self, hostname, fqdn): |
142 | 137 | # Should be FQDN if available. See rc.conf(5) in FreeBSD | 43 | # Should be FQDN if available. See rc.conf(5) in FreeBSD |
143 | 138 | if fqdn: | 44 | if fqdn: |
144 | @@ -140,21 +46,18 @@ class Distro(distros.Distro): | |||
145 | 140 | return hostname | 46 | return hostname |
146 | 141 | 47 | ||
147 | 142 | def _read_system_hostname(self): | 48 | def _read_system_hostname(self): |
150 | 143 | sys_hostname = self._read_hostname(filename=None) | 49 | sys_hostname = self._read_hostname(self.hostname_conf_fn) |
151 | 144 | return ('rc.conf', sys_hostname) | 50 | return (self.hostname_conf_fn, sys_hostname) |
152 | 145 | 51 | ||
153 | 146 | def _read_hostname(self, filename, default=None): | 52 | def _read_hostname(self, filename, default=None): |
160 | 147 | hostname = None | 53 | (_exists, contents) = rhel_util.read_sysconfig_file(filename) |
161 | 148 | try: | 54 | if contents.get('hostname'): |
162 | 149 | hostname = self.readrcconf('hostname') | 55 | return contents['hostname'] |
163 | 150 | except IOError: | 56 | else: |
158 | 151 | pass | ||
159 | 152 | if not hostname: | ||
164 | 153 | return default | 57 | return default |
165 | 154 | return hostname | ||
166 | 155 | 58 | ||
167 | 156 | def _write_hostname(self, hostname, filename): | 59 | def _write_hostname(self, hostname, filename): |
169 | 157 | self.updatercconf('hostname', hostname) | 60 | rhel_util.update_sysconfig_file(filename, {'hostname': hostname}) |
170 | 158 | 61 | ||
171 | 159 | def create_group(self, name, members): | 62 | def create_group(self, name, members): |
172 | 160 | group_add_cmd = ['pw', '-n', name] | 63 | group_add_cmd = ['pw', '-n', name] |
173 | @@ -275,309 +178,16 @@ class Distro(distros.Distro): | |||
174 | 275 | keys = set(kwargs['ssh_authorized_keys']) or [] | 178 | keys = set(kwargs['ssh_authorized_keys']) or [] |
175 | 276 | ssh_util.setup_user_keys(keys, name, options=None) | 179 | ssh_util.setup_user_keys(keys, name, options=None) |
176 | 277 | 180 | ||
177 | 278 | @staticmethod | ||
178 | 279 | def get_ifconfig_list(): | ||
179 | 280 | cmd = ['ifconfig', '-l'] | ||
180 | 281 | (nics, err) = util.subp(cmd, rcs=[0, 1]) | ||
181 | 282 | if len(err): | ||
182 | 283 | LOG.warning("Error running %s: %s", cmd, err) | ||
183 | 284 | return None | ||
184 | 285 | return nics | ||
185 | 286 | |||
186 | 287 | @staticmethod | ||
187 | 288 | def get_ifconfig_ifname_out(ifname): | ||
188 | 289 | cmd = ['ifconfig', ifname] | ||
189 | 290 | (if_result, err) = util.subp(cmd, rcs=[0, 1]) | ||
190 | 291 | if len(err): | ||
191 | 292 | LOG.warning("Error running %s: %s", cmd, err) | ||
192 | 293 | return None | ||
193 | 294 | return if_result | ||
194 | 295 | |||
195 | 296 | @staticmethod | ||
196 | 297 | def get_ifconfig_ether(): | ||
197 | 298 | cmd = ['ifconfig', '-l', 'ether'] | ||
198 | 299 | (nics, err) = util.subp(cmd, rcs=[0, 1]) | ||
199 | 300 | if len(err): | ||
200 | 301 | LOG.warning("Error running %s: %s", cmd, err) | ||
201 | 302 | return None | ||
202 | 303 | return nics | ||
203 | 304 | |||
204 | 305 | @staticmethod | ||
205 | 306 | def get_interface_mac(ifname): | ||
206 | 307 | if_result = Distro.get_ifconfig_ifname_out(ifname) | ||
207 | 308 | for item in if_result.splitlines(): | ||
208 | 309 | if item.find('ether ') != -1: | ||
209 | 310 | mac = str(item.split()[1]) | ||
210 | 311 | if mac: | ||
211 | 312 | return mac | ||
212 | 313 | |||
213 | 314 | @staticmethod | ||
214 | 315 | def get_devicelist(): | ||
215 | 316 | nics = Distro.get_ifconfig_list() | ||
216 | 317 | return nics.split() | ||
217 | 318 | |||
218 | 319 | @staticmethod | ||
219 | 320 | def get_ipv6(): | ||
220 | 321 | ipv6 = [] | ||
221 | 322 | nics = Distro.get_devicelist() | ||
222 | 323 | for nic in nics: | ||
223 | 324 | if_result = Distro.get_ifconfig_ifname_out(nic) | ||
224 | 325 | for item in if_result.splitlines(): | ||
225 | 326 | if item.find("inet6 ") != -1 and item.find("scopeid") == -1: | ||
226 | 327 | ipv6.append(nic) | ||
227 | 328 | return ipv6 | ||
228 | 329 | |||
229 | 330 | def get_ipv4(self): | ||
230 | 331 | ipv4 = [] | ||
231 | 332 | nics = Distro.get_devicelist() | ||
232 | 333 | for nic in nics: | ||
233 | 334 | if_result = Distro.get_ifconfig_ifname_out(nic) | ||
234 | 335 | for item in if_result.splitlines(): | ||
235 | 336 | print(item) | ||
236 | 337 | if self.ipv4_pat.match(item): | ||
237 | 338 | ipv4.append(nic) | ||
238 | 339 | return ipv4 | ||
239 | 340 | |||
240 | 341 | def is_up(self, ifname): | ||
241 | 342 | if_result = Distro.get_ifconfig_ifname_out(ifname) | ||
242 | 343 | pat = "^" + ifname | ||
243 | 344 | for item in if_result.splitlines(): | ||
244 | 345 | if re.match(pat, item): | ||
245 | 346 | flags = item.split('<')[1].split('>')[0] | ||
246 | 347 | if flags.find("UP") != -1: | ||
247 | 348 | return True | ||
248 | 349 | |||
249 | 350 | def _get_current_rename_info(self, check_downable=True): | ||
250 | 351 | """Collect information necessary for rename_interfaces.""" | ||
251 | 352 | names = Distro.get_devicelist() | ||
252 | 353 | bymac = {} | ||
253 | 354 | for n in names: | ||
254 | 355 | bymac[Distro.get_interface_mac(n)] = { | ||
255 | 356 | 'name': n, 'up': self.is_up(n), 'downable': None} | ||
256 | 357 | |||
257 | 358 | nics_with_addresses = set() | ||
258 | 359 | if check_downable: | ||
259 | 360 | nics_with_addresses = set(self.get_ipv4() + self.get_ipv6()) | ||
260 | 361 | |||
261 | 362 | for d in bymac.values(): | ||
262 | 363 | d['downable'] = (d['up'] is False or | ||
263 | 364 | d['name'] not in nics_with_addresses) | ||
264 | 365 | |||
265 | 366 | return bymac | ||
266 | 367 | |||
267 | 368 | def _rename_interfaces(self, renames): | ||
268 | 369 | if not len(renames): | ||
269 | 370 | LOG.debug("no interfaces to rename") | ||
270 | 371 | return | ||
271 | 372 | |||
272 | 373 | current_info = self._get_current_rename_info() | ||
273 | 374 | |||
274 | 375 | cur_bymac = {} | ||
275 | 376 | for mac, data in current_info.items(): | ||
276 | 377 | cur = data.copy() | ||
277 | 378 | cur['mac'] = mac | ||
278 | 379 | cur_bymac[mac] = cur | ||
279 | 380 | |||
280 | 381 | def update_byname(bymac): | ||
281 | 382 | return dict((data['name'], data) | ||
282 | 383 | for data in bymac.values()) | ||
283 | 384 | |||
284 | 385 | def rename(cur, new): | ||
285 | 386 | util.subp(["ifconfig", cur, "name", new], capture=True) | ||
286 | 387 | |||
287 | 388 | def down(name): | ||
288 | 389 | util.subp(["ifconfig", name, "down"], capture=True) | ||
289 | 390 | |||
290 | 391 | def up(name): | ||
291 | 392 | util.subp(["ifconfig", name, "up"], capture=True) | ||
292 | 393 | |||
293 | 394 | ops = [] | ||
294 | 395 | errors = [] | ||
295 | 396 | ups = [] | ||
296 | 397 | cur_byname = update_byname(cur_bymac) | ||
297 | 398 | tmpname_fmt = "cirename%d" | ||
298 | 399 | tmpi = -1 | ||
299 | 400 | |||
300 | 401 | for mac, new_name in renames: | ||
301 | 402 | cur = cur_bymac.get(mac, {}) | ||
302 | 403 | cur_name = cur.get('name') | ||
303 | 404 | cur_ops = [] | ||
304 | 405 | if cur_name == new_name: | ||
305 | 406 | # nothing to do | ||
306 | 407 | continue | ||
307 | 408 | |||
308 | 409 | if not cur_name: | ||
309 | 410 | errors.append("[nic not present] Cannot rename mac=%s to %s" | ||
310 | 411 | ", not available." % (mac, new_name)) | ||
311 | 412 | continue | ||
312 | 413 | |||
313 | 414 | if cur['up']: | ||
314 | 415 | msg = "[busy] Error renaming mac=%s from %s to %s" | ||
315 | 416 | if not cur['downable']: | ||
316 | 417 | errors.append(msg % (mac, cur_name, new_name)) | ||
317 | 418 | continue | ||
318 | 419 | cur['up'] = False | ||
319 | 420 | cur_ops.append(("down", mac, new_name, (cur_name,))) | ||
320 | 421 | ups.append(("up", mac, new_name, (new_name,))) | ||
321 | 422 | |||
322 | 423 | if new_name in cur_byname: | ||
323 | 424 | target = cur_byname[new_name] | ||
324 | 425 | if target['up']: | ||
325 | 426 | msg = "[busy-target] Error renaming mac=%s from %s to %s." | ||
326 | 427 | if not target['downable']: | ||
327 | 428 | errors.append(msg % (mac, cur_name, new_name)) | ||
328 | 429 | continue | ||
329 | 430 | else: | ||
330 | 431 | cur_ops.append(("down", mac, new_name, (new_name,))) | ||
331 | 432 | |||
332 | 433 | tmp_name = None | ||
333 | 434 | while tmp_name is None or tmp_name in cur_byname: | ||
334 | 435 | tmpi += 1 | ||
335 | 436 | tmp_name = tmpname_fmt % tmpi | ||
336 | 437 | |||
337 | 438 | cur_ops.append(("rename", mac, new_name, (new_name, tmp_name))) | ||
338 | 439 | target['name'] = tmp_name | ||
339 | 440 | cur_byname = update_byname(cur_bymac) | ||
340 | 441 | if target['up']: | ||
341 | 442 | ups.append(("up", mac, new_name, (tmp_name,))) | ||
342 | 443 | |||
343 | 444 | cur_ops.append(("rename", mac, new_name, (cur['name'], new_name))) | ||
344 | 445 | cur['name'] = new_name | ||
345 | 446 | cur_byname = update_byname(cur_bymac) | ||
346 | 447 | ops += cur_ops | ||
347 | 448 | |||
348 | 449 | opmap = {'rename': rename, 'down': down, 'up': up} | ||
349 | 450 | if len(ops) + len(ups) == 0: | ||
350 | 451 | if len(errors): | ||
351 | 452 | LOG.debug("unable to do any work for renaming of %s", renames) | ||
352 | 453 | else: | ||
353 | 454 | LOG.debug("no work necessary for renaming of %s", renames) | ||
354 | 455 | else: | ||
355 | 456 | LOG.debug("achieving renaming of %s with ops %s", | ||
356 | 457 | renames, ops + ups) | ||
357 | 458 | |||
358 | 459 | for op, mac, new_name, params in ops + ups: | ||
359 | 460 | try: | ||
360 | 461 | opmap.get(op)(*params) | ||
361 | 462 | except Exception as e: | ||
362 | 463 | errors.append( | ||
363 | 464 | "[unknown] Error performing %s%s for %s, %s: %s" % | ||
364 | 465 | (op, params, mac, new_name, e)) | ||
365 | 466 | if len(errors): | ||
366 | 467 | raise Exception('\n'.join(errors)) | ||
367 | 468 | |||
368 | 469 | def apply_network_config_names(self, netcfg): | ||
369 | 470 | renames = [] | ||
370 | 471 | for ent in netcfg.get('config', {}): | ||
371 | 472 | if ent.get('type') != 'physical': | ||
372 | 473 | continue | ||
373 | 474 | mac = ent.get('mac_address') | ||
374 | 475 | name = ent.get('name') | ||
375 | 476 | if not mac: | ||
376 | 477 | continue | ||
377 | 478 | renames.append([mac, name]) | ||
378 | 479 | return self._rename_interfaces(renames) | ||
379 | 480 | |||
380 | 481 | @classmethod | ||
381 | 482 | def generate_fallback_config(self): | 181 | def generate_fallback_config(self): |
413 | 483 | nics = Distro.get_ifconfig_ether() | 182 | nconf = {'config': [], 'version': 1} |
414 | 484 | if nics is None: | 183 | for mac, name in net.get_interfaces_by_mac().items(): |
384 | 485 | LOG.debug("Fail to get network interfaces") | ||
385 | 486 | return None | ||
386 | 487 | potential_interfaces = nics.split() | ||
387 | 488 | connected = [] | ||
388 | 489 | for nic in potential_interfaces: | ||
389 | 490 | pat = "^" + nic | ||
390 | 491 | if_result = Distro.get_ifconfig_ifname_out(nic) | ||
391 | 492 | for item in if_result.split("\n"): | ||
392 | 493 | if re.match(pat, item): | ||
393 | 494 | flags = item.split('<')[1].split('>')[0] | ||
394 | 495 | if flags.find("RUNNING") != -1: | ||
395 | 496 | connected.append(nic) | ||
396 | 497 | if connected: | ||
397 | 498 | potential_interfaces = connected | ||
398 | 499 | names = list(sorted(potential_interfaces)) | ||
399 | 500 | default_pri_nic = Distro.default_primary_nic | ||
400 | 501 | if default_pri_nic in names: | ||
401 | 502 | names.remove(default_pri_nic) | ||
402 | 503 | names.insert(0, default_pri_nic) | ||
403 | 504 | target_name = None | ||
404 | 505 | target_mac = None | ||
405 | 506 | for name in names: | ||
406 | 507 | mac = Distro.get_interface_mac(name) | ||
407 | 508 | if mac: | ||
408 | 509 | target_name = name | ||
409 | 510 | target_mac = mac | ||
410 | 511 | break | ||
411 | 512 | if target_mac and target_name: | ||
412 | 513 | nconf = {'config': [], 'version': 1} | ||
415 | 514 | nconf['config'].append( | 184 | nconf['config'].append( |
480 | 515 | {'type': 'physical', 'name': target_name, | 185 | {'type': 'physical', 'name': name, |
481 | 516 | 'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]}) | 186 | 'mac_address': mac, 'subnets': [{'type': 'dhcp'}]}) |
482 | 517 | return nconf | 187 | return nconf |
419 | 518 | else: | ||
420 | 519 | return None | ||
421 | 520 | |||
422 | 521 | def _write_network(self, settings): | ||
423 | 522 | entries = net_util.translate_network(settings) | ||
424 | 523 | nameservers = [] | ||
425 | 524 | searchdomains = [] | ||
426 | 525 | dev_names = entries.keys() | ||
427 | 526 | for (device, info) in entries.items(): | ||
428 | 527 | # Skip the loopback interface. | ||
429 | 528 | if device.startswith('lo'): | ||
430 | 529 | continue | ||
431 | 530 | |||
432 | 531 | dev = self.getnetifname(device) | ||
433 | 532 | |||
434 | 533 | LOG.info('Configuring interface %s', dev) | ||
435 | 534 | |||
436 | 535 | if info.get('bootproto') == 'static': | ||
437 | 536 | LOG.debug('Configuring dev %s with %s / %s', dev, | ||
438 | 537 | info.get('address'), info.get('netmask')) | ||
439 | 538 | # Configure an ipv4 address. | ||
440 | 539 | ifconfig = (info.get('address') + ' netmask ' + | ||
441 | 540 | info.get('netmask')) | ||
442 | 541 | |||
443 | 542 | # Configure the gateway. | ||
444 | 543 | self.updatercconf('defaultrouter', info.get('gateway')) | ||
445 | 544 | |||
446 | 545 | if 'dns-nameservers' in info: | ||
447 | 546 | nameservers.extend(info['dns-nameservers']) | ||
448 | 547 | if 'dns-search' in info: | ||
449 | 548 | searchdomains.extend(info['dns-search']) | ||
450 | 549 | else: | ||
451 | 550 | ifconfig = 'DHCP' | ||
452 | 551 | |||
453 | 552 | self.updatercconf('ifconfig_' + dev, ifconfig) | ||
454 | 553 | |||
455 | 554 | # Try to read the /etc/resolv.conf or just start from scratch if that | ||
456 | 555 | # fails. | ||
457 | 556 | try: | ||
458 | 557 | resolvconf = ResolvConf(util.load_file(self.resolv_conf_fn)) | ||
459 | 558 | resolvconf.parse() | ||
460 | 559 | except IOError: | ||
461 | 560 | util.logexc(LOG, "Failed to parse %s, use new empty file", | ||
462 | 561 | self.resolv_conf_fn) | ||
463 | 562 | resolvconf = ResolvConf('') | ||
464 | 563 | resolvconf.parse() | ||
465 | 564 | |||
466 | 565 | # Add some nameservers | ||
467 | 566 | for server in nameservers: | ||
468 | 567 | try: | ||
469 | 568 | resolvconf.add_nameserver(server) | ||
470 | 569 | except ValueError: | ||
471 | 570 | util.logexc(LOG, "Failed to add nameserver %s", server) | ||
472 | 571 | |||
473 | 572 | # And add any searchdomains. | ||
474 | 573 | for domain in searchdomains: | ||
475 | 574 | try: | ||
476 | 575 | resolvconf.add_search_domain(domain) | ||
477 | 576 | except ValueError: | ||
478 | 577 | util.logexc(LOG, "Failed to add search domain %s", domain) | ||
479 | 578 | util.write_file(self.resolv_conf_fn, str(resolvconf), 0o644) | ||
483 | 579 | 188 | ||
485 | 580 | return dev_names | 189 | def _write_network_config(self, netconfig): |
486 | 190 | return self._supported_write_network_config(netconfig) | ||
487 | 581 | 191 | ||
488 | 582 | def apply_locale(self, locale, out_fn=None): | 192 | def apply_locale(self, locale, out_fn=None): |
489 | 583 | # Adjust the locals value to the new value | 193 | # Adjust the locals value to the new value |
490 | @@ -605,18 +215,12 @@ class Distro(distros.Distro): | |||
491 | 605 | util.logexc(LOG, "Failed to restore %s backup", | 215 | util.logexc(LOG, "Failed to restore %s backup", |
492 | 606 | self.login_conf_fn) | 216 | self.login_conf_fn) |
493 | 607 | 217 | ||
506 | 608 | def _bring_up_interface(self, device_name): | 218 | def apply_network_config_names(self, netconfig): |
507 | 609 | if device_name.startswith('lo'): | 219 | # This is handled by the freebsd network renderer. It writes in |
508 | 610 | return | 220 | # /etc/rc.conf a line with the following format: |
509 | 611 | dev = self.getnetifname(device_name) | 221 | # ifconfig_OLDNAME_name=NEWNAME |
510 | 612 | cmd = ['/etc/rc.d/netif', 'start', dev] | 222 | # FreeBSD network script will rename the interface automatically. |
511 | 613 | LOG.debug("Attempting to bring up interface %s using command %s", | 223 | return |
500 | 614 | dev, cmd) | ||
501 | 615 | # This could return 1 when the interface has already been put UP by the | ||
502 | 616 | # OS. This is just fine. | ||
503 | 617 | (_out, err) = util.subp(cmd, rcs=[0, 1]) | ||
504 | 618 | if len(err): | ||
505 | 619 | LOG.warning("Error running %s: %s", cmd, err) | ||
512 | 620 | 224 | ||
513 | 621 | def install_packages(self, pkglist): | 225 | def install_packages(self, pkglist): |
514 | 622 | self.update_package_sources() | 226 | self.update_package_sources() |
515 | diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py | |||
516 | index bd80637..7048f14 100644 | |||
517 | --- a/cloudinit/net/__init__.py | |||
518 | +++ b/cloudinit/net/__init__.py | |||
519 | @@ -765,6 +765,36 @@ def get_ib_interface_hwaddr(ifname, ethernet_format): | |||
520 | 765 | 765 | ||
521 | 766 | 766 | ||
522 | 767 | def get_interfaces_by_mac(): | 767 | def get_interfaces_by_mac(): |
523 | 768 | if util.is_FreeBSD(): | ||
524 | 769 | return get_interfaces_by_mac_on_freebsd() | ||
525 | 770 | else: | ||
526 | 771 | return get_interfaces_by_mac_on_linux() | ||
527 | 772 | |||
528 | 773 | |||
529 | 774 | def get_interfaces_by_mac_on_freebsd(): | ||
530 | 775 | (out, _) = util.subp(['ifconfig', '-a', 'ether']) | ||
531 | 776 | # flatten each interface block in a single line | ||
532 | 777 | def flatten(out): | ||
533 | 778 | curr_block = '' | ||
534 | 779 | for l in out.split('\n'): | ||
535 | 780 | if l.startswith(' '): | ||
536 | 781 | curr_block += l | ||
537 | 782 | else: | ||
538 | 783 | if curr_block: | ||
539 | 784 | yield curr_block | ||
540 | 785 | curr_block = l | ||
541 | 786 | yield curr_block | ||
542 | 787 | # looks for interface and mac in a list of flatten block | ||
543 | 788 | def find_mac(flat_list): | ||
544 | 789 | for block in flat_list: | ||
545 | 790 | m = re.search(r"^(\S*): .*ether\s([\da-f:]{17}).*", block) | ||
546 | 791 | if m: | ||
547 | 792 | yield (m.group(1), m.group(2)) | ||
548 | 793 | results = {i[0]: i[1] for i in find_mac(flatten(out))} | ||
549 | 794 | return results | ||
550 | 795 | |||
551 | 796 | |||
552 | 797 | def get_interfaces_by_mac_on_linux(): | ||
553 | 768 | """Build a dictionary of tuples {mac: name}. | 798 | """Build a dictionary of tuples {mac: name}. |
554 | 769 | 799 | ||
555 | 770 | Bridges and any devices that have a 'stolen' mac are excluded.""" | 800 | Bridges and any devices that have a 'stolen' mac are excluded.""" |
556 | diff --git a/cloudinit/net/freebsd.py b/cloudinit/net/freebsd.py | |||
557 | 771 | new file mode 100644 | 801 | new file mode 100644 |
558 | index 0000000..066d5eb | |||
559 | --- /dev/null | |||
560 | +++ b/cloudinit/net/freebsd.py | |||
561 | @@ -0,0 +1,123 @@ | |||
562 | 1 | # This file is part of cloud-init. See LICENSE file for license information. | ||
563 | 2 | |||
564 | 3 | import re | ||
565 | 4 | |||
566 | 5 | from cloudinit import log as logging | ||
567 | 6 | from cloudinit import net | ||
568 | 7 | from cloudinit import util | ||
569 | 8 | from cloudinit.distros import rhel_util | ||
570 | 9 | from cloudinit.distros.parsers.resolv_conf import ResolvConf | ||
571 | 10 | |||
572 | 11 | from . import renderer | ||
573 | 12 | |||
574 | 13 | LOG = logging.getLogger(__name__) | ||
575 | 14 | |||
576 | 15 | |||
577 | 16 | class Renderer(renderer.Renderer): | ||
578 | 17 | resolv_conf_fn = 'etc/resolv.conf' | ||
579 | 18 | rc_conf_fn = 'etc/rc.conf' | ||
580 | 19 | |||
581 | 20 | def __init__(self, config=None): | ||
582 | 21 | if not config: | ||
583 | 22 | config = {} | ||
584 | 23 | self.dhcp_interfaces = [] | ||
585 | 24 | self._postcmds = config.get('postcmds', True) | ||
586 | 25 | |||
587 | 26 | def _render_route(self, route, indent=""): | ||
588 | 27 | pass | ||
589 | 28 | |||
590 | 29 | def _render_iface(self, iface, render_hwaddress=False): | ||
591 | 30 | pass | ||
592 | 31 | |||
593 | 32 | def _write_network(self, settings, target=None): | ||
594 | 33 | nameservers = [] | ||
595 | 34 | searchdomains = [] | ||
596 | 35 | ifname_by_mac = net.get_interfaces_by_mac() | ||
597 | 36 | for interface in settings.iter_interfaces(): | ||
598 | 37 | device_name = interface.get("name") | ||
599 | 38 | device_mac = interface.get("mac_address") | ||
600 | 39 | if device_name and re.match(r'^lo\d+$', device_name): | ||
601 | 40 | continue | ||
602 | 41 | if device_mac and device_name: | ||
603 | 42 | cur_name = ifname_by_mac[device_mac] | ||
604 | 43 | if not cur_name: | ||
605 | 44 | LOG.info('Cannot find any device with MAC %s', device_mac) | ||
606 | 45 | continue | ||
607 | 46 | if cur_name != device_name: | ||
608 | 47 | rhel_util.update_sysconfig_file( | ||
609 | 48 | util.target_path(target, self.rc_conf_fn), { | ||
610 | 49 | 'ifconfig_%s_name' % cur_name: device_name}) | ||
611 | 50 | elif device_mac: | ||
612 | 51 | device_name = ifname_by_mac[device_mac] | ||
613 | 52 | |||
614 | 53 | subnet = interface.get("subnets", [])[0] | ||
615 | 54 | LOG.info('Configuring interface %s', device_name) | ||
616 | 55 | |||
617 | 56 | if subnet.get('type') == 'static': | ||
618 | 57 | LOG.debug('Configuring dev %s with %s / %s', device_name, | ||
619 | 58 | subnet.get('address'), subnet.get('netmask')) | ||
620 | 59 | # Configure an ipv4 address. | ||
621 | 60 | ifconfig = (subnet.get('address') + ' netmask ' + | ||
622 | 61 | subnet.get('netmask')) | ||
623 | 62 | |||
624 | 63 | # Configure the gateway. | ||
625 | 64 | rhel_util.update_sysconfig_file( | ||
626 | 65 | util.target_path(target, self.rc_conf_fn), { | ||
627 | 66 | 'defaultrouter': subnet.get('gateway')}) | ||
628 | 67 | |||
629 | 68 | if 'dns_nameservers' in subnet: | ||
630 | 69 | nameservers.extend(subnet['dns_nameservers']) | ||
631 | 70 | if 'dns_search' in subnet: | ||
632 | 71 | searchdomains.extend(subnet['dns_search']) | ||
633 | 72 | else: | ||
634 | 73 | self.dhcp_interfaces.append(device_name) | ||
635 | 74 | ifconfig = 'DHCP' | ||
636 | 75 | |||
637 | 76 | rhel_util.update_sysconfig_file( | ||
638 | 77 | util.target_path(target, self.rc_conf_fn), { | ||
639 | 78 | 'ifconfig_' + device_name: ifconfig}) | ||
640 | 79 | |||
641 | 80 | # Try to read the /etc/resolv.conf or just start from scratch if that | ||
642 | 81 | # fails. | ||
643 | 82 | try: | ||
644 | 83 | resolvconf = ResolvConf(util.load_file(self.resolv_conf_fn)) | ||
645 | 84 | resolvconf.parse() | ||
646 | 85 | except IOError: | ||
647 | 86 | util.logexc(LOG, "Failed to parse %s, use new empty file", | ||
648 | 87 | self.resolv_conf_fn) | ||
649 | 88 | resolvconf = ResolvConf('') | ||
650 | 89 | resolvconf.parse() | ||
651 | 90 | |||
652 | 91 | # Add some nameservers | ||
653 | 92 | for server in nameservers: | ||
654 | 93 | try: | ||
655 | 94 | resolvconf.add_nameserver(server) | ||
656 | 95 | except ValueError: | ||
657 | 96 | util.logexc(LOG, "Failed to add nameserver %s", server) | ||
658 | 97 | |||
659 | 98 | # And add any searchdomains. | ||
660 | 99 | for domain in searchdomains: | ||
661 | 100 | try: | ||
662 | 101 | resolvconf.add_search_domain(domain) | ||
663 | 102 | except ValueError: | ||
664 | 103 | util.logexc(LOG, "Failed to add search domain %s", domain) | ||
665 | 104 | util.write_file(self.resolv_conf_fn, str(resolvconf), 0o644) | ||
666 | 105 | self.start_services() | ||
667 | 106 | |||
668 | 107 | def render_network_state(self, network_state, templates=None, target=None): | ||
669 | 108 | self._write_network(network_state, target=target) | ||
670 | 109 | |||
671 | 110 | def start_services(self): | ||
672 | 111 | if not self._postcmds: | ||
673 | 112 | LOG.debug("freebsd generate postcmd disabled") | ||
674 | 113 | return | ||
675 | 114 | |||
676 | 115 | util.subp(['service', 'netif', 'restart'], capture=True) | ||
677 | 116 | util.subp(['service', 'routing', 'restart'], capture=True) | ||
678 | 117 | for dhcp_interface in self.dhcp_interfaces: | ||
679 | 118 | util.subp(['service', 'dhclient', 'restart', dhcp_interface], | ||
680 | 119 | capture=True) | ||
681 | 120 | |||
682 | 121 | |||
683 | 122 | def available(target=None): | ||
684 | 123 | return util.is_FreeBSD() | ||
685 | diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py | |||
686 | index 5117b4a..b98dbbe 100644 | |||
687 | --- a/cloudinit/net/renderers.py | |||
688 | +++ b/cloudinit/net/renderers.py | |||
689 | @@ -1,17 +1,19 @@ | |||
690 | 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. |
691 | 2 | 2 | ||
692 | 3 | from . import eni | 3 | from . import eni |
693 | 4 | from . import freebsd | ||
694 | 4 | from . import netplan | 5 | from . import netplan |
695 | 5 | from . import RendererNotFoundError | 6 | from . import RendererNotFoundError |
696 | 6 | from . import sysconfig | 7 | from . import sysconfig |
697 | 7 | 8 | ||
698 | 8 | NAME_TO_RENDERER = { | 9 | NAME_TO_RENDERER = { |
699 | 9 | "eni": eni, | 10 | "eni": eni, |
700 | 11 | "freebsd": freebsd, | ||
701 | 10 | "netplan": netplan, | 12 | "netplan": netplan, |
702 | 11 | "sysconfig": sysconfig, | 13 | "sysconfig": sysconfig, |
703 | 12 | } | 14 | } |
704 | 13 | 15 | ||
706 | 14 | DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan"] | 16 | DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan", "freebsd"] |
707 | 15 | 17 | ||
708 | 16 | 18 | ||
709 | 17 | def search(priority=None, target=None, first=False): | 19 | def search(priority=None, target=None, first=False): |
710 | diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst | |||
711 | index 51ced4d..1520ba9 100644 | |||
712 | --- a/doc/rtd/topics/network-config.rst | |||
713 | +++ b/doc/rtd/topics/network-config.rst | |||
714 | @@ -191,7 +191,7 @@ supplying an updated configuration in cloud-config. :: | |||
715 | 191 | 191 | ||
716 | 192 | system_info: | 192 | system_info: |
717 | 193 | network: | 193 | network: |
719 | 194 | renderers: ['netplan', 'eni', 'sysconfig'] | 194 | renderers: ['netplan', 'eni', 'sysconfig', 'freebsd'] |
720 | 195 | 195 | ||
721 | 196 | 196 | ||
722 | 197 | Network Configuration Tools | 197 | Network Configuration Tools |
723 | diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py | |||
724 | index 6720995..4c14d8c 100644 | |||
725 | --- a/tests/unittests/test_distros/test_netconfig.py | |||
726 | +++ b/tests/unittests/test_distros/test_netconfig.py | |||
727 | @@ -1,5 +1,6 @@ | |||
728 | 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. |
729 | 2 | 2 | ||
730 | 3 | import copy | ||
731 | 3 | import os | 4 | import os |
732 | 4 | from six import StringIO | 5 | from six import StringIO |
733 | 5 | from textwrap import dedent | 6 | from textwrap import dedent |
734 | @@ -14,7 +15,7 @@ from cloudinit.distros.parsers.sys_conf import SysConf | |||
735 | 14 | from cloudinit import helpers | 15 | from cloudinit import helpers |
736 | 15 | from cloudinit import settings | 16 | from cloudinit import settings |
737 | 16 | from cloudinit.tests.helpers import ( | 17 | from cloudinit.tests.helpers import ( |
739 | 17 | FilesystemMockingTestCase, dir2dict, populate_dir) | 18 | FilesystemMockingTestCase, dir2dict) |
740 | 18 | from cloudinit import util | 19 | from cloudinit import util |
741 | 19 | 20 | ||
742 | 20 | 21 | ||
743 | @@ -213,128 +214,95 @@ class TestNetCfgDistroBase(FilesystemMockingTestCase): | |||
744 | 213 | self.assertEqual(v, b2[k]) | 214 | self.assertEqual(v, b2[k]) |
745 | 214 | 215 | ||
746 | 215 | 216 | ||
748 | 216 | class TestNetCfgDistroFreebsd(TestNetCfgDistroBase): | 217 | class TestNetCfgDistroFreeBSD(TestNetCfgDistroBase): |
749 | 217 | 218 | ||
759 | 218 | frbsd_ifout = """\ | 219 | def setUp(self): |
760 | 219 | hn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500 | 220 | super(TestNetCfgDistroFreeBSD, self).setUp() |
761 | 220 | options=51b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,TSO4,LRO> | 221 | self.distro = self._get_distro('freebsd', renderers=['freebsd']) |
762 | 221 | ether 00:15:5d:4c:73:00 | 222 | |
763 | 222 | inet6 fe80::215:5dff:fe4c:7300%hn0 prefixlen 64 scopeid 0x2 | 223 | def _apply_and_verify_freebsd(self, apply_fn, config, expected_cfgs=None, |
764 | 223 | inet 10.156.76.127 netmask 0xfffffc00 broadcast 10.156.79.255 | 224 | bringup=False): |
765 | 224 | nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL> | 225 | if not expected_cfgs: |
766 | 225 | media: Ethernet autoselect (10Gbase-T <full-duplex>) | 226 | raise ValueError('expected_cfg must not be None') |
767 | 226 | status: active | 227 | |
768 | 228 | tmpd = None | ||
769 | 229 | with mock.patch('cloudinit.net.freebsd.available') as m_avail: | ||
770 | 230 | m_avail.return_value = True | ||
771 | 231 | with self.reRooted(tmpd) as tmpd: | ||
772 | 232 | util.ensure_dir('/etc') | ||
773 | 233 | util.ensure_file('/etc/rc.conf') | ||
774 | 234 | util.ensure_file('/etc/resolv.conf') | ||
775 | 235 | apply_fn(config, bringup) | ||
776 | 236 | |||
777 | 237 | results = dir2dict(tmpd) | ||
778 | 238 | for cfgpath, expected in expected_cfgs.items(): | ||
779 | 239 | print("----------") | ||
780 | 240 | print(expected) | ||
781 | 241 | print("^^^^ expected | rendered VVVVVVV") | ||
782 | 242 | print(results[cfgpath]) | ||
783 | 243 | print("----------") | ||
784 | 244 | self.assertEqual( | ||
785 | 245 | set(expected.split('\n')), | ||
786 | 246 | set(results[cfgpath].split('\n'))) | ||
787 | 247 | self.assertEqual(0o644, get_mode(cfgpath, tmpd)) | ||
788 | 248 | |||
789 | 249 | @mock.patch('cloudinit.net.get_interfaces_by_mac') | ||
790 | 250 | def test_apply_network_config_freebsd_standard(self, ifaces_mac): | ||
791 | 251 | ifaces_mac.return_value = { | ||
792 | 252 | '00:15:5d:4c:73:00': 'eth0', | ||
793 | 253 | } | ||
794 | 254 | rc_conf_expected = """\ | ||
795 | 255 | defaultrouter=192.168.1.254 | ||
796 | 256 | ifconfig_eth0='192.168.1.5 netmask 255.255.255.0' | ||
797 | 257 | ifconfig_eth1=DHCP | ||
798 | 227 | """ | 258 | """ |
799 | 228 | 259 | ||
830 | 229 | @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_list') | 260 | expected_cfgs = { |
831 | 230 | @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ifname_out') | 261 | '/etc/rc.conf': rc_conf_expected, |
832 | 231 | def test_get_ip_nic_freebsd(self, ifname_out, iflist): | 262 | '/etc/resolv.conf': '' |
833 | 232 | frbsd_distro = self._get_distro('freebsd') | 263 | } |
834 | 233 | iflist.return_value = "lo0 hn0" | 264 | self._apply_and_verify_freebsd(self.distro.apply_network_config, |
835 | 234 | ifname_out.return_value = self.frbsd_ifout | 265 | V1_NET_CFG, |
836 | 235 | res = frbsd_distro.get_ipv4() | 266 | expected_cfgs=expected_cfgs.copy()) |
837 | 236 | self.assertEqual(res, ['lo0', 'hn0']) | 267 | |
838 | 237 | res = frbsd_distro.get_ipv6() | 268 | @mock.patch('cloudinit.net.get_interfaces_by_mac') |
839 | 238 | self.assertEqual(res, []) | 269 | def test_apply_network_config_freebsd_ifrename(self, ifaces_mac): |
840 | 239 | 270 | ifaces_mac.return_value = { | |
841 | 240 | @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ether') | 271 | '00:15:5d:4c:73:00': 'vtnet0', |
812 | 241 | @mock.patch('cloudinit.distros.freebsd.Distro.get_ifconfig_ifname_out') | ||
813 | 242 | @mock.patch('cloudinit.distros.freebsd.Distro.get_interface_mac') | ||
814 | 243 | def test_generate_fallback_config_freebsd(self, mac, ifname_out, if_ether): | ||
815 | 244 | frbsd_distro = self._get_distro('freebsd') | ||
816 | 245 | |||
817 | 246 | if_ether.return_value = 'hn0' | ||
818 | 247 | ifname_out.return_value = self.frbsd_ifout | ||
819 | 248 | mac.return_value = '00:15:5d:4c:73:00' | ||
820 | 249 | res = frbsd_distro.generate_fallback_config() | ||
821 | 250 | self.assertIsNotNone(res) | ||
822 | 251 | |||
823 | 252 | def test_simple_write_freebsd(self): | ||
824 | 253 | fbsd_distro = self._get_distro('freebsd') | ||
825 | 254 | |||
826 | 255 | rc_conf = '/etc/rc.conf' | ||
827 | 256 | read_bufs = { | ||
828 | 257 | rc_conf: 'initial-rc-conf-not-validated', | ||
829 | 258 | '/etc/resolv.conf': 'initial-resolv-conf-not-validated', | ||
842 | 259 | } | 272 | } |
843 | 273 | rc_conf_expected = """\ | ||
844 | 274 | ifconfig_vtnet0_name=eth0 | ||
845 | 275 | defaultrouter=192.168.1.254 | ||
846 | 276 | ifconfig_eth0='192.168.1.5 netmask 255.255.255.0' | ||
847 | 277 | ifconfig_eth1=DHCP | ||
848 | 278 | """ | ||
849 | 260 | 279 | ||
874 | 261 | tmpd = self.tmp_dir() | 280 | V1_NET_CFG_RENAME = copy.deepcopy(V1_NET_CFG) |
875 | 262 | populate_dir(tmpd, read_bufs) | 281 | V1_NET_CFG_RENAME['config'][0]['mac_address'] = '00:15:5d:4c:73:00' |
876 | 263 | with self.reRooted(tmpd): | 282 | |
877 | 264 | with mock.patch("cloudinit.distros.freebsd.util.subp", | 283 | expected_cfgs = { |
878 | 265 | return_value=('vtnet0', '')): | 284 | '/etc/rc.conf': rc_conf_expected, |
879 | 266 | fbsd_distro.apply_network(BASE_NET_CFG, False) | 285 | '/etc/resolv.conf': '' |
856 | 267 | results = dir2dict(tmpd) | ||
857 | 268 | |||
858 | 269 | self.assertIn(rc_conf, results) | ||
859 | 270 | self.assertCfgEquals( | ||
860 | 271 | dedent('''\ | ||
861 | 272 | ifconfig_vtnet0="192.168.1.5 netmask 255.255.255.0" | ||
862 | 273 | ifconfig_vtnet1="DHCP" | ||
863 | 274 | defaultrouter="192.168.1.254" | ||
864 | 275 | '''), results[rc_conf]) | ||
865 | 276 | self.assertEqual(0o644, get_mode(rc_conf, tmpd)) | ||
866 | 277 | |||
867 | 278 | def test_simple_write_freebsd_from_v2eni(self): | ||
868 | 279 | fbsd_distro = self._get_distro('freebsd') | ||
869 | 280 | |||
870 | 281 | rc_conf = '/etc/rc.conf' | ||
871 | 282 | read_bufs = { | ||
872 | 283 | rc_conf: 'initial-rc-conf-not-validated', | ||
873 | 284 | '/etc/resolv.conf': 'initial-resolv-conf-not-validated', | ||
880 | 285 | } | 286 | } |
881 | 287 | self._apply_and_verify_freebsd(self.distro.apply_network_config, | ||
882 | 288 | V1_NET_CFG_RENAME, | ||
883 | 289 | expected_cfgs=expected_cfgs.copy()) | ||
884 | 286 | 290 | ||
923 | 287 | tmpd = self.tmp_dir() | 291 | @mock.patch('cloudinit.net.get_interfaces_by_mac') |
924 | 288 | populate_dir(tmpd, read_bufs) | 292 | def test_apply_network_config_freebsd_nameserver(self, ifaces_mac): |
925 | 289 | with self.reRooted(tmpd): | 293 | ifaces_mac.return_value = { |
926 | 290 | with mock.patch("cloudinit.distros.freebsd.util.subp", | 294 | '00:15:5d:4c:73:00': 'eth0', |
889 | 291 | return_value=('vtnet0', '')): | ||
890 | 292 | fbsd_distro.apply_network(BASE_NET_CFG_FROM_V2, False) | ||
891 | 293 | results = dir2dict(tmpd) | ||
892 | 294 | |||
893 | 295 | self.assertIn(rc_conf, results) | ||
894 | 296 | self.assertCfgEquals( | ||
895 | 297 | dedent('''\ | ||
896 | 298 | ifconfig_vtnet0="192.168.1.5 netmask 255.255.255.0" | ||
897 | 299 | ifconfig_vtnet1="DHCP" | ||
898 | 300 | defaultrouter="192.168.1.254" | ||
899 | 301 | '''), results[rc_conf]) | ||
900 | 302 | self.assertEqual(0o644, get_mode(rc_conf, tmpd)) | ||
901 | 303 | |||
902 | 304 | def test_apply_network_config_fallback_freebsd(self): | ||
903 | 305 | fbsd_distro = self._get_distro('freebsd') | ||
904 | 306 | |||
905 | 307 | # a weak attempt to verify that we don't have an implementation | ||
906 | 308 | # of _write_network_config or apply_network_config in fbsd now, | ||
907 | 309 | # which would make this test not actually test the fallback. | ||
908 | 310 | self.assertRaises( | ||
909 | 311 | NotImplementedError, fbsd_distro._write_network_config, | ||
910 | 312 | BASE_NET_CFG) | ||
911 | 313 | |||
912 | 314 | # now run | ||
913 | 315 | mynetcfg = { | ||
914 | 316 | 'config': [{"type": "physical", "name": "eth0", | ||
915 | 317 | "mac_address": "c0:d6:9f:2c:e8:80", | ||
916 | 318 | "subnets": [{"type": "dhcp"}]}], | ||
917 | 319 | 'version': 1} | ||
918 | 320 | |||
919 | 321 | rc_conf = '/etc/rc.conf' | ||
920 | 322 | read_bufs = { | ||
921 | 323 | rc_conf: 'initial-rc-conf-not-validated', | ||
922 | 324 | '/etc/resolv.conf': 'initial-resolv-conf-not-validated', | ||
927 | 325 | } | 295 | } |
928 | 326 | 296 | ||
940 | 327 | tmpd = self.tmp_dir() | 297 | V1_NET_CFG_DNS = copy.deepcopy(V1_NET_CFG) |
941 | 328 | populate_dir(tmpd, read_bufs) | 298 | ns = ['1.2.3.4'] |
942 | 329 | with self.reRooted(tmpd): | 299 | V1_NET_CFG_DNS['config'][0]['subnets'][0]['dns_nameservers'] = ns |
943 | 330 | with mock.patch("cloudinit.distros.freebsd.util.subp", | 300 | expected_cfgs = { |
944 | 331 | return_value=('vtnet0', '')): | 301 | '/etc/resolv.conf': 'nameserver 1.2.3.4\n' |
945 | 332 | fbsd_distro.apply_network_config(mynetcfg, bring_up=False) | 302 | } |
946 | 333 | results = dir2dict(tmpd) | 303 | self._apply_and_verify_freebsd(self.distro.apply_network_config, |
947 | 334 | 304 | V1_NET_CFG_DNS, | |
948 | 335 | self.assertIn(rc_conf, results) | 305 | expected_cfgs=expected_cfgs.copy()) |
938 | 336 | self.assertCfgEquals('ifconfig_vtnet0="DHCP"', results[rc_conf]) | ||
939 | 337 | self.assertEqual(0o644, get_mode(rc_conf, tmpd)) | ||
949 | 338 | 306 | ||
950 | 339 | 307 | ||
951 | 340 | class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase): | 308 | class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase): |
952 | diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py | |||
953 | index 0f45dc3..47a50e7 100644 | |||
954 | --- a/tests/unittests/test_net.py | |||
955 | +++ b/tests/unittests/test_net.py | |||
956 | @@ -2409,7 +2409,6 @@ DEFAULT_DEV_ATTRS = { | |||
957 | 2409 | } | 2409 | } |
958 | 2410 | } | 2410 | } |
959 | 2411 | 2411 | ||
960 | 2412 | |||
961 | 2413 | def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net, | 2412 | def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net, |
962 | 2414 | mock_sys_dev_path, dev_attrs=None): | 2413 | mock_sys_dev_path, dev_attrs=None): |
963 | 2415 | if not dev_attrs: | 2414 | if not dev_attrs: |
964 | diff --git a/tests/unittests/test_net_freebsd.py b/tests/unittests/test_net_freebsd.py | |||
965 | 2416 | new file mode 100644 | 2415 | new file mode 100644 |
966 | index 0000000..8adb14e | |||
967 | --- /dev/null | |||
968 | +++ b/tests/unittests/test_net_freebsd.py | |||
969 | @@ -0,0 +1,29 @@ | |||
970 | 1 | from cloudinit import net | ||
971 | 2 | |||
972 | 3 | from cloudinit.tests.helpers import (CiTestCase, mock) | ||
973 | 4 | |||
974 | 5 | |||
975 | 6 | IFCONFIG_FREEBSD = """vtnet0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500 | ||
976 | 7 | options=6c07bb<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,JUMBO_MTU,VLAN_HWCSUM,TSO4,TSO6,LRO,VLAN_HWTSO,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6> | ||
977 | 8 | ether 52:54:00:50:b7:0d | ||
978 | 9 | re0.33: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> metric 0 mtu 1500 | ||
979 | 10 | options=80003<RXCSUM,TXCSUM,LINKSTATE> | ||
980 | 11 | ether 80:00:73:63:5c:48 | ||
981 | 12 | groups: vlan | ||
982 | 13 | vlan: 33 vlanpcp: 0 parent interface: re0 | ||
983 | 14 | media: Ethernet autoselect (1000baseT <full-duplex,master>) | ||
984 | 15 | status: active | ||
985 | 16 | nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL> | ||
986 | 17 | lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384 | ||
987 | 18 | options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6> | ||
988 | 19 | """ | ||
989 | 20 | |||
990 | 21 | class TestInterfacesByMac(CiTestCase): | ||
991 | 22 | |||
992 | 23 | @mock.patch('cloudinit.util.subp') | ||
993 | 24 | @mock.patch('cloudinit.util.is_FreeBSD') | ||
994 | 25 | def test_get_interfaces_by_mac(self, mock_is_FreeBSD, mock_subp): | ||
995 | 26 | mock_is_FreeBSD.return_value = True | ||
996 | 27 | mock_subp.return_value = (IFCONFIG_FREEBSD, 0) | ||
997 | 28 | a = net.get_interfaces_by_mac() | ||
998 | 29 | assert a == {"vtnet0": "52:54:00:50:b7:0d", "re0.33": "80:00:73:63:5c:48"} |
Thanks for refactoring the BSD support. I've a few comments inline.