Merge ~raharper/cloud-init:azure_run_local into cloud-init:master
- Git
- lp:~raharper/cloud-init
- azure_run_local
- Merge into master
Status: | Merged |
---|---|
Merged at revision: | 4d9f24f5c385cb7fa21d87a097ccd9a297613a75 |
Proposed branch: | ~raharper/cloud-init:azure_run_local |
Merge into: | cloud-init:master |
Diff against target: |
1105 lines (+775/-45) 8 files modified
cloudinit/net/__init__.py (+154/-27) cloudinit/net/eni.py (+2/-0) cloudinit/net/renderer.py (+3/-1) cloudinit/net/udev.py (+5/-2) cloudinit/sources/DataSourceAzure.py (+61/-0) tests/unittests/test_datasource/test_azure.py (+86/-0) tests/unittests/test_datasource/test_common.py (+1/-0) tests/unittests/test_net.py (+463/-15) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
Scott Moser | Pending | ||
Review via email: mp+326099@code.launchpad.net |
Commit message
Description of the change
Refactor net layer handling of duplicate macs, add Azure network-config
On systems with network devices with duplicate mac addresses, cloud-init
will fail to rename the devices according to the specified network configuration. Refactor net layer to search by device driver and
device id if available
Update Azure datasource to run at init-local time and let Azure datasource generate a fallback networking config to handle advanced
networking configurations
Scott Moser (smoser) wrote : | # |
Ryan Harper (raharper) wrote : | # |
Looking at the boot log[1], we now generate networking twice (once in init-local and again in init-net). Not sure how best to handle that. Work to do is still needed around duplicate mac assumption in cloudinit.net (renames fail, and it needs a tuple like (name,mac,driver) to distinguish; possibly more since there may be more than two VFs with the same mac and same driver).
Ryan Harper (raharper) wrote : | # |
> There are some comments in line. Others here.
>
> 1. I'd have thought that there would need to be a fix for the 'duplicate mac
> address detected' as we see
> https:/
That;s still needed, but currently the rename of the interfaces isn't the first priority (not breaking existing instances which don't have duplicate macs is step one).
>
> I'm not sure how that is being avoided. It seems even if we don't call
> 'get_interfaces
> up calling that function at some point in some generic code path and on azure
> where there might be duplicate macs, it will fail.
> A solution would be to change callers i guess to accept that there *might* be
> identical macs.
Yes, still looking for unique attributes.
>
> B. It seems like there is duplicate code in DataSourceAzureNet and
> DataSourceAzure. I'd like to have just one class, i think we should be able to
> do that. I suspect that might be just work-in-progress.
Yes, the other class can be dropped since I now run Net class in both modes.
>
> C. It seems to me that you've added support for Azure being able to provide
> config that includes a device driver (in 'network config v1 format'). But
> generically, I do not believe that is present in v1 or v2/netplan format. If
> that *is* true, then there isn't actually a way that those formats can specify
> this configuration, and we should at least open issues to that affect.
v2 supports driver under the Match key, v1 does not; we should likely update v1 to support it without the param trickery.
We'll likely also want (long term)
1) control in the datasource over whether we generate udev rules or not (this is available but not exposed, i.e. if you set the link_path in the renderer class to None, they don't get generated)
2) v1/v2 config might also need to express additional attributes to include in the udev rule (like driver) but also a rule_name field, in our case, we may refer to a device via a tuple( vf1=(vf1,
>
> D. network_config will still only be applied once per instance. So If a user
> shuts down an instance and attaches a device and starts it back up, I do not
> think we will generate a different/updated config. Is that right? Its likely
> i'm not understanding something.
That's something else we'll need to address in the next stage, we're trying to make sure instances booted with duplicate macs don't comeup without any networking at all.
>
>
> E. When testing this please make sure you test:
> i.) test upgrade and reboot on same instance. This will test the path where
> there exists a /var/lib/
> DataSourceAzureNet.
Ack.
> ii.) test upgrade and reboot with clean of /var/log/cloud* /var/lib/cloud-
> init* and cloud_config entries in /etc/fstab. (mocking a clean instance)
Ack, I've already tested this, but will look back to (i) now that (ii) is known working.
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:bb4af3069aa
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
- ca42187... by Ryan Harper
-
Fix copy and paste error which clobbered device/driver config
- a05e831... by Ryan Harper
-
azure: implement a bond configuration if we find duplicate macs
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:a05e8315769
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
- 5105963... by Ryan Harper
-
Don't allow bonds into fallback network-config
- b0b3257... by Ryan Harper
-
flake8 fixes
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:b0b32579671
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
SUCCESS: CentOS 6 & 7: Build & Test
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
- f31c8a0... by Ryan Harper
-
Disable azure bonding fallback config generation.
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:f31c8a02f75
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
SUCCESS: CentOS 6 & 7: Build & Test
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) : | # |
Ryan Harper (raharper) wrote : | # |
On Mon, Jun 26, 2017 at 2:11 PM, Scott Moser <email address hidden> wrote:
>
>
> Diff comments:
>
> > diff --git a/cloudinit/
> > index 65accbb..7e3333f 100644
> > --- a/cloudinit/
> > +++ b/cloudinit/
> > @@ -124,6 +128,26 @@ def is_present(
> > return os.path.
> >
> >
> > +def device_
> > + """Return the device driver for net device named 'devname' """
>
> Add a period at end, adn remove trailing whitespace in the comment.
>
ACK
>
> > + driver = None
> > + driver_path = sys_dev_
> > + # driver is a symlink to the driver *dir*
> > + if os.path.
> > + driver = os.path.
> > +
> > + return driver
> > +
> > +
> > +def device_
> > + """Return the device id string for net device named 'devname' """
> > + dev_id = read_sys_
> > + if dev_id is False:
> > + return None
> > +
> > + return dev_id
> > +
> > +
> > def get_devicelist():
> > return os.listdir(
> >
> > @@ -304,14 +367,44 @@ def _rename_
> strict_
> > ops = []
> > errors = []
> > ups = []
> > - cur_byname = update_
> > + cur_byname = update_
> > tmpname_fmt = "cirename%d"
> > tmpi = -1
> >
> > - for mac, new_name in renames:
> > - cur = cur_bymac.get(mac, {})
> > - cur_name = cur.get('name')
> > + def entry_match(data, mac, driver, device_id):
> > + """match if set and in data"""
> > + if mac and driver and device_id:
> > + return (data['mac'] == mac and
> > + data['driver'] == driver and
> > + data['device_id'] == device_id)
> > + elif mac and driver:
> > + return (data['mac'] == mac and
> > + data['driver'] == driver)
> > + elif mac:
> > + return (data['mac'] == mac)
> > +
> > + return False
> > +
> > + def find_entry(mac, driver, device_id):
> > + match = [data for data in cur_info.values()
> > + if entry_match(data, mac, driver, device_id)]
> > + print("%s: %s" % (len(match), match))
>
> print
>
ACK
>
> > + if len(match):
> > + return match.pop()
>
> return match[0]
> ?
> since you dont do anything else with 'match', pop is not useful i dont
> hink.
>
> Should we raise exception if we have more than one in 'match' ?
> Otherwise we're just silently picking one based on the sort of a dict.
>
I've not encountered duplicate mac, duplicate driver *and* duplicate
device_id (save if the values of driver and devid are None)
So, yes I Think raising an exception may be the right thing here since it's
unexpected.
Alternatively we could select the first and warn with the list of what else
matched.
>
> > +
> > + return None
> > +
> > + for mac, new_name, driver, device_id in renames:
> > cur_ops = []
> > + cur = find_entry(mac, driver, devic...
- 1cedcbc... by Ryan Harper
-
Fix docstring punctuation. Raise exception on matching multiple devices
- df7874a... by Ryan Harper
-
Fix spelling errors in comment block.
Ryan Harper (raharper) wrote : | # |
Updated with fixes for ACK'ed issues. Let's decide what to do with get_interfaces_
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:df7874a9ece
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
SUCCESS: CentOS 6 & 7: Build & Test
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
I have to fix some test issues but
i think my branch at
https:/
does what we are looking for.
the unit tests all fail because they are expecting that 'get_data' does the negotiation with the fabric. but other than that my commit d87ba732d966fdb
I'll continue to work from there tomorrow.
Preview Diff
1 | diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py |
2 | index 65accbb..cba991a 100644 |
3 | --- a/cloudinit/net/__init__.py |
4 | +++ b/cloudinit/net/__init__.py |
5 | @@ -97,6 +97,10 @@ def is_bridge(devname): |
6 | return os.path.exists(sys_dev_path(devname, "bridge")) |
7 | |
8 | |
9 | +def is_bond(devname): |
10 | + return os.path.exists(sys_dev_path(devname, "bonding")) |
11 | + |
12 | + |
13 | def is_vlan(devname): |
14 | uevent = str(read_sys_net_safe(devname, "uevent")) |
15 | return 'DEVTYPE=vlan' in uevent.splitlines() |
16 | @@ -124,6 +128,26 @@ def is_present(devname): |
17 | return os.path.exists(sys_dev_path(devname)) |
18 | |
19 | |
20 | +def device_driver(devname): |
21 | + """Return the device driver for net device named 'devname'.""" |
22 | + driver = None |
23 | + driver_path = sys_dev_path(devname, "device/driver") |
24 | + # driver is a symlink to the driver *dir* |
25 | + if os.path.islink(driver_path): |
26 | + driver = os.path.basename(os.readlink(driver_path)) |
27 | + |
28 | + return driver |
29 | + |
30 | + |
31 | +def device_devid(devname): |
32 | + """Return the device id string for net device named 'devname'.""" |
33 | + dev_id = read_sys_net_safe(devname, "device/device") |
34 | + if dev_id is False: |
35 | + return None |
36 | + |
37 | + return dev_id |
38 | + |
39 | + |
40 | def get_devicelist(): |
41 | return os.listdir(SYS_CLASS_NET) |
42 | |
43 | @@ -138,12 +162,21 @@ def is_disabled_cfg(cfg): |
44 | return cfg.get('config') == "disabled" |
45 | |
46 | |
47 | -def generate_fallback_config(): |
48 | +def generate_fallback_config(blacklist_drivers=None, config_driver=None): |
49 | """Determine which attached net dev is most likely to have a connection and |
50 | generate network state to run dhcp on that interface""" |
51 | + |
52 | + if not config_driver: |
53 | + config_driver = False |
54 | + |
55 | + if not blacklist_drivers: |
56 | + blacklist_drivers = [] |
57 | + |
58 | # get list of interfaces that could have connections |
59 | invalid_interfaces = set(['lo']) |
60 | - potential_interfaces = set(get_devicelist()) |
61 | + potential_interfaces = set([device for device in get_devicelist() |
62 | + if device_driver(device) not in |
63 | + blacklist_drivers]) |
64 | potential_interfaces = potential_interfaces.difference(invalid_interfaces) |
65 | # sort into interfaces with carrier, interfaces which could have carrier, |
66 | # and ignore interfaces that are definitely disconnected |
67 | @@ -155,6 +188,9 @@ def generate_fallback_config(): |
68 | if is_bridge(interface): |
69 | # skip any bridges |
70 | continue |
71 | + if is_bond(interface): |
72 | + # skip any bonds |
73 | + continue |
74 | carrier = read_sys_net_int(interface, 'carrier') |
75 | if carrier: |
76 | connected.append(interface) |
77 | @@ -194,9 +230,18 @@ def generate_fallback_config(): |
78 | break |
79 | if target_mac and target_name: |
80 | nconf = {'config': [], 'version': 1} |
81 | - nconf['config'].append( |
82 | - {'type': 'physical', 'name': target_name, |
83 | - 'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]}) |
84 | + cfg = {'type': 'physical', 'name': target_name, |
85 | + 'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]} |
86 | + # inject the device driver name, dev_id into config if enabled and |
87 | + # device has a valid device driver value |
88 | + if config_driver: |
89 | + driver = device_driver(target_name) |
90 | + if driver: |
91 | + cfg['params'] = { |
92 | + 'driver': driver, |
93 | + 'device_id': device_devid(target_name), |
94 | + } |
95 | + nconf['config'].append(cfg) |
96 | return nconf |
97 | else: |
98 | # can't read any interfaces addresses (or there are none); give up |
99 | @@ -217,10 +262,16 @@ def apply_network_config_names(netcfg, strict_present=True, strict_busy=True): |
100 | if ent.get('type') != 'physical': |
101 | continue |
102 | mac = ent.get('mac_address') |
103 | - name = ent.get('name') |
104 | if not mac: |
105 | continue |
106 | - renames.append([mac, name]) |
107 | + name = ent.get('name') |
108 | + driver = ent.get('params', {}).get('driver') |
109 | + device_id = ent.get('params', {}).get('device_id') |
110 | + if not driver: |
111 | + driver = device_driver(name) |
112 | + if not device_id: |
113 | + device_id = device_devid(name) |
114 | + renames.append([mac, name, driver, device_id]) |
115 | |
116 | return _rename_interfaces(renames) |
117 | |
118 | @@ -245,15 +296,27 @@ def _get_current_rename_info(check_downable=True): |
119 | """Collect information necessary for rename_interfaces. |
120 | |
121 | returns a dictionary by mac address like: |
122 | - {mac: |
123 | - {'name': name |
124 | - 'up': boolean: is_up(name), |
125 | + {name: |
126 | + { |
127 | 'downable': None or boolean indicating that the |
128 | - device has only automatically assigned ip addrs.}} |
129 | + device has only automatically assigned ip addrs. |
130 | + 'device_id': Device id value (if it has one) |
131 | + 'driver': Device driver (if it has one) |
132 | + 'mac': mac address |
133 | + 'name': name |
134 | + 'up': boolean: is_up(name) |
135 | + }} |
136 | """ |
137 | - bymac = {} |
138 | - for mac, name in get_interfaces_by_mac().items(): |
139 | - bymac[mac] = {'name': name, 'up': is_up(name), 'downable': None} |
140 | + cur_info = {} |
141 | + for (name, mac, driver, device_id) in get_interfaces(): |
142 | + cur_info[name] = { |
143 | + 'downable': None, |
144 | + 'device_id': device_id, |
145 | + 'driver': driver, |
146 | + 'mac': mac, |
147 | + 'name': name, |
148 | + 'up': is_up(name), |
149 | + } |
150 | |
151 | if check_downable: |
152 | nmatch = re.compile(r"[0-9]+:\s+(\w+)[@:]") |
153 | @@ -265,11 +328,11 @@ def _get_current_rename_info(check_downable=True): |
154 | for bytes_out in (ipv6, ipv4): |
155 | nics_with_addresses.update(nmatch.findall(bytes_out)) |
156 | |
157 | - for d in bymac.values(): |
158 | + for d in cur_info.values(): |
159 | d['downable'] = (d['up'] is False or |
160 | d['name'] not in nics_with_addresses) |
161 | |
162 | - return bymac |
163 | + return cur_info |
164 | |
165 | |
166 | def _rename_interfaces(renames, strict_present=True, strict_busy=True, |
167 | @@ -282,15 +345,15 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True, |
168 | if current_info is None: |
169 | current_info = _get_current_rename_info() |
170 | |
171 | - cur_bymac = {} |
172 | - for mac, data in current_info.items(): |
173 | + cur_info = {} |
174 | + for name, data in current_info.items(): |
175 | cur = data.copy() |
176 | - cur['mac'] = mac |
177 | - cur_bymac[mac] = cur |
178 | + cur['name'] = name |
179 | + cur_info[name] = cur |
180 | |
181 | def update_byname(bymac): |
182 | return dict((data['name'], data) |
183 | - for data in bymac.values()) |
184 | + for data in cur_info.values()) |
185 | |
186 | def rename(cur, new): |
187 | util.subp(["ip", "link", "set", cur, "name", new], capture=True) |
188 | @@ -304,14 +367,48 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True, |
189 | ops = [] |
190 | errors = [] |
191 | ups = [] |
192 | - cur_byname = update_byname(cur_bymac) |
193 | + cur_byname = update_byname(cur_info) |
194 | tmpname_fmt = "cirename%d" |
195 | tmpi = -1 |
196 | |
197 | - for mac, new_name in renames: |
198 | - cur = cur_bymac.get(mac, {}) |
199 | - cur_name = cur.get('name') |
200 | + def entry_match(data, mac, driver, device_id): |
201 | + """match if set and in data""" |
202 | + if mac and driver and device_id: |
203 | + return (data['mac'] == mac and |
204 | + data['driver'] == driver and |
205 | + data['device_id'] == device_id) |
206 | + elif mac and driver: |
207 | + return (data['mac'] == mac and |
208 | + data['driver'] == driver) |
209 | + elif mac: |
210 | + return (data['mac'] == mac) |
211 | + |
212 | + return False |
213 | + |
214 | + def find_entry(mac, driver, device_id): |
215 | + match = [data for data in cur_info.values() |
216 | + if entry_match(data, mac, driver, device_id)] |
217 | + if len(match): |
218 | + if len(match) > 1: |
219 | + msg = ('Failed to match a single device. Matched devices "%s"' |
220 | + ' with search values "(mac:%s driver:%s device_id:%s)"' |
221 | + % (match, mac, driver, device_id)) |
222 | + raise ValueError(msg) |
223 | + return match[0] |
224 | + |
225 | + return None |
226 | + |
227 | + for mac, new_name, driver, device_id in renames: |
228 | cur_ops = [] |
229 | + cur = find_entry(mac, driver, device_id) |
230 | + if not cur: |
231 | + if strict_present: |
232 | + errors.append( |
233 | + "[nic not present] Cannot rename mac=%s to %s" |
234 | + ", not available." % (mac, new_name)) |
235 | + continue |
236 | + |
237 | + cur_name = cur.get('name') |
238 | if cur_name == new_name: |
239 | # nothing to do |
240 | continue |
241 | @@ -351,13 +448,13 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True, |
242 | |
243 | cur_ops.append(("rename", mac, new_name, (new_name, tmp_name))) |
244 | target['name'] = tmp_name |
245 | - cur_byname = update_byname(cur_bymac) |
246 | + cur_byname = update_byname(cur_info) |
247 | if target['up']: |
248 | ups.append(("up", mac, new_name, (tmp_name,))) |
249 | |
250 | cur_ops.append(("rename", mac, new_name, (cur['name'], new_name))) |
251 | cur['name'] = new_name |
252 | - cur_byname = update_byname(cur_bymac) |
253 | + cur_byname = update_byname(cur_info) |
254 | ops += cur_ops |
255 | |
256 | opmap = {'rename': rename, 'down': down, 'up': up} |
257 | @@ -426,6 +523,36 @@ def get_interfaces_by_mac(): |
258 | return ret |
259 | |
260 | |
261 | +def get_interfaces(): |
262 | + """Return list of interface tuples (name, mac, driver, device_id) |
263 | + |
264 | + Bridges and any devices that have a 'stolen' mac are excluded.""" |
265 | + try: |
266 | + devs = get_devicelist() |
267 | + except OSError as e: |
268 | + if e.errno == errno.ENOENT: |
269 | + devs = [] |
270 | + else: |
271 | + raise |
272 | + ret = [] |
273 | + empty_mac = '00:00:00:00:00:00' |
274 | + for name in devs: |
275 | + if not interface_has_own_mac(name): |
276 | + continue |
277 | + if is_bridge(name): |
278 | + continue |
279 | + if is_vlan(name): |
280 | + continue |
281 | + mac = get_interface_mac(name) |
282 | + # some devices may not have a mac (tun0) |
283 | + if not mac: |
284 | + continue |
285 | + if mac == empty_mac and name != 'lo': |
286 | + continue |
287 | + ret.append((name, mac, device_driver(name), device_devid(name))) |
288 | + return ret |
289 | + |
290 | + |
291 | class RendererNotFoundError(RuntimeError): |
292 | pass |
293 | |
294 | diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py |
295 | index 98ce01e..b707146 100644 |
296 | --- a/cloudinit/net/eni.py |
297 | +++ b/cloudinit/net/eni.py |
298 | @@ -72,6 +72,8 @@ def _iface_add_attrs(iface, index): |
299 | content = [] |
300 | ignore_map = [ |
301 | 'control', |
302 | + 'device_id', |
303 | + 'driver', |
304 | 'index', |
305 | 'inet', |
306 | 'mode', |
307 | diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py |
308 | index c68658d..bba139e 100644 |
309 | --- a/cloudinit/net/renderer.py |
310 | +++ b/cloudinit/net/renderer.py |
311 | @@ -34,8 +34,10 @@ class Renderer(object): |
312 | for iface in network_state.iter_interfaces(filter_by_physical): |
313 | # for physical interfaces write out a persist net udev rule |
314 | if 'name' in iface and iface.get('mac_address'): |
315 | + driver = iface.get('driver', None) |
316 | content.write(generate_udev_rule(iface['name'], |
317 | - iface['mac_address'])) |
318 | + iface['mac_address'], |
319 | + driver=driver)) |
320 | return content.getvalue() |
321 | |
322 | @abc.abstractmethod |
323 | diff --git a/cloudinit/net/udev.py b/cloudinit/net/udev.py |
324 | index fd2fd8c..58c0a70 100644 |
325 | --- a/cloudinit/net/udev.py |
326 | +++ b/cloudinit/net/udev.py |
327 | @@ -23,7 +23,7 @@ def compose_udev_setting(key, value): |
328 | return '%s="%s"' % (key, value) |
329 | |
330 | |
331 | -def generate_udev_rule(interface, mac): |
332 | +def generate_udev_rule(interface, mac, driver=None): |
333 | """Return a udev rule to set the name of network interface with `mac`. |
334 | |
335 | The rule ends up as a single line looking something like: |
336 | @@ -31,10 +31,13 @@ def generate_udev_rule(interface, mac): |
337 | SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", |
338 | ATTR{address}="ff:ee:dd:cc:bb:aa", NAME="eth0" |
339 | """ |
340 | + if not driver: |
341 | + driver = '?*' |
342 | + |
343 | rule = ', '.join([ |
344 | compose_udev_equality('SUBSYSTEM', 'net'), |
345 | compose_udev_equality('ACTION', 'add'), |
346 | - compose_udev_equality('DRIVERS', '?*'), |
347 | + compose_udev_equality('DRIVERS', driver), |
348 | compose_udev_attr_equality('address', mac), |
349 | compose_udev_setting('NAME', interface), |
350 | ]) |
351 | diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py |
352 | index 4fe0d63..13ec5a0 100644 |
353 | --- a/cloudinit/sources/DataSourceAzure.py |
354 | +++ b/cloudinit/sources/DataSourceAzure.py |
355 | @@ -16,6 +16,7 @@ from xml.dom import minidom |
356 | import xml.etree.ElementTree as ET |
357 | |
358 | from cloudinit import log as logging |
359 | +from cloudinit import net |
360 | from cloudinit import sources |
361 | from cloudinit.sources.helpers.azure import get_metadata_from_fabric |
362 | from cloudinit import util |
363 | @@ -255,6 +256,7 @@ class DataSourceAzureNet(sources.DataSource): |
364 | util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}), |
365 | BUILTIN_DS_CONFIG]) |
366 | self.dhclient_lease_file = self.ds_cfg.get('dhclient_lease_file') |
367 | + self._network_config = None |
368 | |
369 | def __str__(self): |
370 | root = sources.DataSource.__str__(self) |
371 | @@ -331,6 +333,14 @@ class DataSourceAzureNet(sources.DataSource): |
372 | if asset_tag != AZURE_CHASSIS_ASSET_TAG: |
373 | LOG.debug("Non-Azure DMI asset tag '%s' discovered.", asset_tag) |
374 | return False |
375 | + |
376 | + self.dsmode = self._determine_dsmode(get_dsmodes(self._network_config)) |
377 | + if self.dsmode == sources.DSMODE_LOCAL: |
378 | + self.metadata['instance-id'] = util.read_dmi_data('system-uuid') |
379 | + # switch dsmode to network so init-net can run as well |
380 | + self.dsmode = sources.DSMODE_NETWORK |
381 | + return True |
382 | + |
383 | ddir = self.ds_cfg['data_dir'] |
384 | |
385 | candidates = [self.seed_dir] |
386 | @@ -423,6 +433,50 @@ class DataSourceAzureNet(sources.DataSource): |
387 | address_ephemeral_resize(is_new_instance=is_new_instance) |
388 | return |
389 | |
390 | + @property |
391 | + def network_config(self): |
392 | + """Generate a network config like net.generate_fallback_network() with |
393 | + the following execptions. |
394 | + |
395 | + 1. Probe the drivers of the net-devices present and inject them in |
396 | + the network configuration under params: driver: <driver> value |
397 | + 2. If the driver value is 'mlx4_core', the control mode should be |
398 | + set to manual. The device will be later used to build a bond, |
399 | + for now we want to ensure the device gets named but does not |
400 | + break any network configuration |
401 | + """ |
402 | + blacklist = ['mlx4_core'] |
403 | + if not self._network_config: |
404 | + LOG.debug('Azure: generating fallback configuration') |
405 | + # generate a network config, blacklist picking any mlx4_core devs |
406 | + netconfig = net.generate_fallback_config( |
407 | + blacklist_drivers=blacklist, config_driver=True) |
408 | + |
409 | + # if we have any blacklisted devices, update the network_config to |
410 | + # include the device, mac, and driver values, but with no ip |
411 | + # config; this ensures udev rules are generated but won't affect |
412 | + # ip configuration |
413 | + bl_found = 0 |
414 | + for bl_dev in [dev for dev in net.get_devicelist() |
415 | + if net.device_driver(dev) in blacklist]: |
416 | + bl_found += 1 |
417 | + cfg = { |
418 | + 'type': 'physical', |
419 | + 'name': 'vf%d' % bl_found, |
420 | + 'mac_address': net.get_interface_mac(bl_dev), |
421 | + 'params': { |
422 | + 'driver': net.device_driver(bl_dev), |
423 | + 'device_id': net.device_devid(bl_dev), |
424 | + }, |
425 | + } |
426 | + netconfig['config'].append(cfg) |
427 | + |
428 | + # switch to network mode after generating a config |
429 | + self.dsmode = sources.DSMODE_NETWORK |
430 | + self._network_config = netconfig |
431 | + |
432 | + return self._network_config |
433 | + |
434 | |
435 | def _partitions_on_device(devpath, maxnum=16): |
436 | # return a list of tuples (ptnum, path) for each part on devpath |
437 | @@ -641,6 +695,12 @@ def invoke_agent(cmd): |
438 | LOG.debug("not invoking agent") |
439 | |
440 | |
441 | +def get_dsmodes(netconfig=None): |
442 | + """Return which dsmode we use be in based on current network config""" |
443 | + return [sources.DSMODE_LOCAL |
444 | + if not netconfig else sources.DSMODE_NETWORK] |
445 | + |
446 | + |
447 | def find_child(node, filter_func): |
448 | ret = [] |
449 | if not node.hasChildNodes(): |
450 | @@ -851,6 +911,7 @@ class NonAzureDataSource(Exception): |
451 | |
452 | # Used to match classes to dependencies |
453 | datasources = [ |
454 | + (DataSourceAzureNet, (sources.DEP_FILESYSTEM, )), |
455 | (DataSourceAzureNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), |
456 | ] |
457 | |
458 | diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py |
459 | index 7d33daf..66f9d77 100644 |
460 | --- a/tests/unittests/test_datasource/test_azure.py |
461 | +++ b/tests/unittests/test_datasource/test_azure.py |
462 | @@ -142,6 +142,10 @@ scbus-1 on xpt0 bus 0 |
463 | def _invoke_agent(cmd): |
464 | data['agent_invoked'] = cmd |
465 | |
466 | + def _get_dsmodes(config): |
467 | + # sources.DSMODE_NETWORK |
468 | + return 'net' |
469 | + |
470 | def _wait_for_files(flist, _maxwait=None, _naplen=None): |
471 | data['waited'] = flist |
472 | return [] |
473 | @@ -171,6 +175,7 @@ scbus-1 on xpt0 bus 0 |
474 | self.apply_patches([ |
475 | (dsaz, 'list_possible_azure_ds_devs', dsdevs), |
476 | (dsaz, 'invoke_agent', _invoke_agent), |
477 | + (dsaz, 'get_dsmodes', _get_dsmodes), |
478 | (dsaz, 'wait_for_files', _wait_for_files), |
479 | (dsaz, 'pubkeys_from_crt_files', _pubkeys_from_crt_files), |
480 | (dsaz, 'perform_hostname_bounce', mock.MagicMock()), |
481 | @@ -554,6 +559,84 @@ fdescfs /dev/fd fdescfs rw 0 0 |
482 | self.assertEqual( |
483 | [mock.call("/dev/cd0")], m_check_fbsd_cdrom.call_args_list) |
484 | |
485 | + @mock.patch('cloudinit.net.get_interface_mac') |
486 | + @mock.patch('cloudinit.net.get_devicelist') |
487 | + @mock.patch('cloudinit.net.device_driver') |
488 | + @mock.patch('cloudinit.net.generate_fallback_config') |
489 | + def test_network_config(self, mock_fallback, mock_dd, |
490 | + mock_devlist, mock_get_mac): |
491 | + odata = {'HostName': "myhost", 'UserName': "myuser"} |
492 | + data = {'ovfcontent': construct_valid_ovf_env(data=odata), |
493 | + 'sys_cfg': {}} |
494 | + |
495 | + fallback_config = { |
496 | + 'version': 1, |
497 | + 'config': [{ |
498 | + 'type': 'physical', 'name': 'eth0', |
499 | + 'mac_address': '00:11:22:33:44:55', |
500 | + 'params': {'driver': 'hv_netsvc'}, |
501 | + 'subnets': [{'type': 'dhcp'}], |
502 | + }] |
503 | + } |
504 | + mock_fallback.return_value = fallback_config |
505 | + |
506 | + mock_devlist.return_value = ['eth0'] |
507 | + mock_dd.return_value = ['hv_netsvc'] |
508 | + mock_get_mac.return_value = '00:11:22:33:44:55' |
509 | + |
510 | + dsrc = self._get_ds(data) |
511 | + ret = dsrc.get_data() |
512 | + self.assertTrue(ret) |
513 | + |
514 | + netconfig = dsrc.network_config |
515 | + self.assertEqual(netconfig, fallback_config) |
516 | + mock_fallback.assert_called_with(blacklist_drivers=['mlx4_core'], |
517 | + config_driver=True) |
518 | + |
519 | + @mock.patch('cloudinit.net.get_interface_mac') |
520 | + @mock.patch('cloudinit.net.get_devicelist') |
521 | + @mock.patch('cloudinit.net.device_driver') |
522 | + @mock.patch('cloudinit.net.generate_fallback_config') |
523 | + def test_network_config_blacklist(self, mock_fallback, mock_dd, |
524 | + mock_devlist, mock_get_mac): |
525 | + odata = {'HostName': "myhost", 'UserName': "myuser"} |
526 | + data = {'ovfcontent': construct_valid_ovf_env(data=odata), |
527 | + 'sys_cfg': {}} |
528 | + |
529 | + fallback_config = { |
530 | + 'version': 1, |
531 | + 'config': [{ |
532 | + 'type': 'physical', 'name': 'eth0', |
533 | + 'mac_address': '00:11:22:33:44:55', |
534 | + 'params': {'driver': 'hv_netsvc'}, |
535 | + 'subnets': [{'type': 'dhcp'}], |
536 | + }] |
537 | + } |
538 | + blacklist_config = { |
539 | + 'type': 'physical', |
540 | + 'name': 'eth1', |
541 | + 'mac_address': '00:11:22:33:44:55', |
542 | + 'params': {'driver': 'mlx4_core'} |
543 | + } |
544 | + mock_fallback.return_value = fallback_config |
545 | + |
546 | + mock_devlist.return_value = ['eth0', 'eth1'] |
547 | + mock_dd.side_effect = [ |
548 | + 'hv_netsvc', # list composition, skipped |
549 | + 'mlx4_core', # list composition, match |
550 | + 'mlx4_core', # config get driver name |
551 | + ] |
552 | + mock_get_mac.return_value = '00:11:22:33:44:55' |
553 | + |
554 | + dsrc = self._get_ds(data) |
555 | + ret = dsrc.get_data() |
556 | + self.assertTrue(ret) |
557 | + |
558 | + netconfig = dsrc.network_config |
559 | + expected_config = fallback_config |
560 | + expected_config['config'].append(blacklist_config) |
561 | + self.assertEqual(netconfig, expected_config) |
562 | + |
563 | |
564 | class TestAzureBounce(TestCase): |
565 | |
566 | @@ -568,6 +651,9 @@ class TestAzureBounce(TestCase): |
567 | self.patches.enter_context( |
568 | mock.patch.object(dsaz, 'get_metadata_from_fabric', |
569 | mock.MagicMock(return_value={}))) |
570 | + self.patches.enter_context( |
571 | + mock.patch.object(dsaz, 'get_dsmodes', |
572 | + mock.MagicMock(return_value='net'))) |
573 | |
574 | def _dmi_mocks(key): |
575 | if key == 'system-uuid': |
576 | diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/test_datasource/test_common.py |
577 | index 7649b9a..be60781 100644 |
578 | --- a/tests/unittests/test_datasource/test_common.py |
579 | +++ b/tests/unittests/test_datasource/test_common.py |
580 | @@ -26,6 +26,7 @@ from cloudinit.sources import DataSourceNone as DSNone |
581 | from .. import helpers as test_helpers |
582 | |
583 | DEFAULT_LOCAL = [ |
584 | + Azure.DataSourceAzureNet, |
585 | CloudSigma.DataSourceCloudSigma, |
586 | ConfigDrive.DataSourceConfigDrive, |
587 | DigitalOcean.DataSourceDigitalOcean, |
588 | diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py |
589 | index 8edc0b8..06e8f09 100644 |
590 | --- a/tests/unittests/test_net.py |
591 | +++ b/tests/unittests/test_net.py |
592 | @@ -836,38 +836,176 @@ CONFIG_V1_EXPLICIT_LOOPBACK = { |
593 | 'subnets': [{'control': 'auto', 'type': 'loopback'}]}, |
594 | ]} |
595 | |
596 | +DEFAULT_DEV_ATTRS = { |
597 | + 'eth1000': { |
598 | + "bridge": False, |
599 | + "carrier": False, |
600 | + "dormant": False, |
601 | + "operstate": "down", |
602 | + "address": "07-1C-C6-75-A4-BE", |
603 | + "device/driver": None, |
604 | + "device/device": None, |
605 | + } |
606 | +} |
607 | + |
608 | |
609 | def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net, |
610 | - mock_sys_dev_path): |
611 | - mock_get_devicelist.return_value = ['eth1000'] |
612 | - dev_characteristics = { |
613 | - 'eth1000': { |
614 | - "bridge": False, |
615 | - "carrier": False, |
616 | - "dormant": False, |
617 | - "operstate": "down", |
618 | - "address": "07-1C-C6-75-A4-BE", |
619 | - } |
620 | - } |
621 | + mock_sys_dev_path, dev_attrs=None): |
622 | + if not dev_attrs: |
623 | + dev_attrs = DEFAULT_DEV_ATTRS |
624 | + |
625 | + mock_get_devicelist.return_value = dev_attrs.keys() |
626 | |
627 | def fake_read(devname, path, translate=None, |
628 | on_enoent=None, on_keyerror=None, |
629 | on_einval=None): |
630 | - return dev_characteristics[devname][path] |
631 | + return dev_attrs[devname][path] |
632 | |
633 | mock_read_sys_net.side_effect = fake_read |
634 | |
635 | def sys_dev_path(devname, path=""): |
636 | - return tmp_dir + devname + "/" + path |
637 | + return tmp_dir + "/" + devname + "/" + path |
638 | |
639 | - for dev in dev_characteristics: |
640 | + for dev in dev_attrs: |
641 | os.makedirs(os.path.join(tmp_dir, dev)) |
642 | with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh: |
643 | - fh.write("down") |
644 | + fh.write(dev_attrs[dev]['operstate']) |
645 | + os.makedirs(os.path.join(tmp_dir, dev, "device")) |
646 | + for key in ['device/driver']: |
647 | + if key in dev_attrs[dev] and dev_attrs[dev][key]: |
648 | + target = dev_attrs[dev][key] |
649 | + link = os.path.join(tmp_dir, dev, key) |
650 | + print('symlink %s -> %s' % (link, target)) |
651 | + os.symlink(target, link) |
652 | |
653 | mock_sys_dev_path.side_effect = sys_dev_path |
654 | |
655 | |
656 | +class TestGenerateFallbackConfig(CiTestCase): |
657 | + |
658 | + @mock.patch("cloudinit.net.sys_dev_path") |
659 | + @mock.patch("cloudinit.net.read_sys_net") |
660 | + @mock.patch("cloudinit.net.get_devicelist") |
661 | + def test_device_driver(self, mock_get_devicelist, mock_read_sys_net, |
662 | + mock_sys_dev_path): |
663 | + devices = { |
664 | + 'eth0': { |
665 | + 'bridge': False, 'carrier': False, 'dormant': False, |
666 | + 'operstate': 'down', 'address': '00:11:22:33:44:55', |
667 | + 'device/driver': 'hv_netsvc', 'device/device': '0x3'}, |
668 | + 'eth1': { |
669 | + 'bridge': False, 'carrier': False, 'dormant': False, |
670 | + 'operstate': 'down', 'address': '00:11:22:33:44:55', |
671 | + 'device/driver': 'mlx4_core', 'device/device': '0x7'}, |
672 | + } |
673 | + |
674 | + tmp_dir = self.tmp_dir() |
675 | + _setup_test(tmp_dir, mock_get_devicelist, |
676 | + mock_read_sys_net, mock_sys_dev_path, |
677 | + dev_attrs=devices) |
678 | + |
679 | + network_cfg = net.generate_fallback_config(config_driver=True) |
680 | + ns = network_state.parse_net_config_data(network_cfg, |
681 | + skip_broken=False) |
682 | + |
683 | + render_dir = os.path.join(tmp_dir, "render") |
684 | + os.makedirs(render_dir) |
685 | + |
686 | + # don't set rulepath so eni writes them |
687 | + renderer = eni.Renderer( |
688 | + {'eni_path': 'interfaces', 'netrules_path': 'netrules'}) |
689 | + renderer.render_network_state(ns, render_dir) |
690 | + |
691 | + self.assertTrue(os.path.exists(os.path.join(render_dir, |
692 | + 'interfaces'))) |
693 | + with open(os.path.join(render_dir, 'interfaces')) as fh: |
694 | + contents = fh.read() |
695 | + print(contents) |
696 | + expected = """ |
697 | +auto lo |
698 | +iface lo inet loopback |
699 | + |
700 | +auto eth0 |
701 | +iface eth0 inet dhcp |
702 | +""" |
703 | + self.assertEqual(expected.lstrip(), contents.lstrip()) |
704 | + |
705 | + self.assertTrue(os.path.exists(os.path.join(render_dir, 'netrules'))) |
706 | + with open(os.path.join(render_dir, 'netrules')) as fh: |
707 | + contents = fh.read() |
708 | + print(contents) |
709 | + expected_rule = [ |
710 | + 'SUBSYSTEM=="net"', |
711 | + 'ACTION=="add"', |
712 | + 'DRIVERS=="hv_netsvc"', |
713 | + 'ATTR{address}=="00:11:22:33:44:55"', |
714 | + 'NAME="eth0"', |
715 | + ] |
716 | + self.assertEqual(", ".join(expected_rule) + '\n', contents.lstrip()) |
717 | + |
718 | + @mock.patch("cloudinit.net.sys_dev_path") |
719 | + @mock.patch("cloudinit.net.read_sys_net") |
720 | + @mock.patch("cloudinit.net.get_devicelist") |
721 | + def test_device_driver_blacklist(self, mock_get_devicelist, |
722 | + mock_read_sys_net, mock_sys_dev_path): |
723 | + devices = { |
724 | + 'eth1': { |
725 | + 'bridge': False, 'carrier': False, 'dormant': False, |
726 | + 'operstate': 'down', 'address': '00:11:22:33:44:55', |
727 | + 'device/driver': 'hv_netsvc', 'device/device': '0x3'}, |
728 | + 'eth0': { |
729 | + 'bridge': False, 'carrier': False, 'dormant': False, |
730 | + 'operstate': 'down', 'address': '00:11:22:33:44:55', |
731 | + 'device/driver': 'mlx4_core', 'device/device': '0x7'}, |
732 | + } |
733 | + |
734 | + tmp_dir = self.tmp_dir() |
735 | + _setup_test(tmp_dir, mock_get_devicelist, |
736 | + mock_read_sys_net, mock_sys_dev_path, |
737 | + dev_attrs=devices) |
738 | + |
739 | + blacklist = ['mlx4_core'] |
740 | + network_cfg = net.generate_fallback_config(blacklist_drivers=blacklist, |
741 | + config_driver=True) |
742 | + ns = network_state.parse_net_config_data(network_cfg, |
743 | + skip_broken=False) |
744 | + |
745 | + render_dir = os.path.join(tmp_dir, "render") |
746 | + os.makedirs(render_dir) |
747 | + |
748 | + # don't set rulepath so eni writes them |
749 | + renderer = eni.Renderer( |
750 | + {'eni_path': 'interfaces', 'netrules_path': 'netrules'}) |
751 | + renderer.render_network_state(ns, render_dir) |
752 | + |
753 | + self.assertTrue(os.path.exists(os.path.join(render_dir, |
754 | + 'interfaces'))) |
755 | + with open(os.path.join(render_dir, 'interfaces')) as fh: |
756 | + contents = fh.read() |
757 | + print(contents) |
758 | + expected = """ |
759 | +auto lo |
760 | +iface lo inet loopback |
761 | + |
762 | +auto eth1 |
763 | +iface eth1 inet dhcp |
764 | +""" |
765 | + self.assertEqual(expected.lstrip(), contents.lstrip()) |
766 | + |
767 | + self.assertTrue(os.path.exists(os.path.join(render_dir, 'netrules'))) |
768 | + with open(os.path.join(render_dir, 'netrules')) as fh: |
769 | + contents = fh.read() |
770 | + print(contents) |
771 | + expected_rule = [ |
772 | + 'SUBSYSTEM=="net"', |
773 | + 'ACTION=="add"', |
774 | + 'DRIVERS=="hv_netsvc"', |
775 | + 'ATTR{address}=="00:11:22:33:44:55"', |
776 | + 'NAME="eth1"', |
777 | + ] |
778 | + self.assertEqual(", ".join(expected_rule) + '\n', contents.lstrip()) |
779 | + |
780 | + |
781 | class TestSysConfigRendering(CiTestCase): |
782 | |
783 | @mock.patch("cloudinit.net.sys_dev_path") |
784 | @@ -1560,6 +1698,118 @@ class TestNetRenderers(CiTestCase): |
785 | priority=['sysconfig', 'eni']) |
786 | |
787 | |
788 | +class TestGetInterfaces(CiTestCase): |
789 | + _data = {'bonds': ['bond1'], |
790 | + 'bridges': ['bridge1'], |
791 | + 'vlans': ['bond1.101'], |
792 | + 'own_macs': ['enp0s1', 'enp0s2', 'bridge1-nic', 'bridge1', |
793 | + 'bond1.101', 'lo', 'eth1'], |
794 | + 'macs': {'enp0s1': 'aa:aa:aa:aa:aa:01', |
795 | + 'enp0s2': 'aa:aa:aa:aa:aa:02', |
796 | + 'bond1': 'aa:aa:aa:aa:aa:01', |
797 | + 'bond1.101': 'aa:aa:aa:aa:aa:01', |
798 | + 'bridge1': 'aa:aa:aa:aa:aa:03', |
799 | + 'bridge1-nic': 'aa:aa:aa:aa:aa:03', |
800 | + 'lo': '00:00:00:00:00:00', |
801 | + 'greptap0': '00:00:00:00:00:00', |
802 | + 'eth1': 'aa:aa:aa:aa:aa:01', |
803 | + 'tun0': None}, |
804 | + 'drivers': {'enp0s1': 'virtio_net', |
805 | + 'enp0s2': 'e1000', |
806 | + 'bond1': None, |
807 | + 'bond1.101': None, |
808 | + 'bridge1': None, |
809 | + 'bridge1-nic': None, |
810 | + 'lo': None, |
811 | + 'greptap0': None, |
812 | + 'eth1': 'mlx4_core', |
813 | + 'tun0': None}} |
814 | + data = {} |
815 | + |
816 | + def _se_get_devicelist(self): |
817 | + return list(self.data['devices']) |
818 | + |
819 | + def _se_device_driver(self, name): |
820 | + return self.data['drivers'][name] |
821 | + |
822 | + def _se_device_devid(self, name): |
823 | + return '0x%s' % sorted(list(self.data['drivers'].keys())).index(name) |
824 | + |
825 | + def _se_get_interface_mac(self, name): |
826 | + return self.data['macs'][name] |
827 | + |
828 | + def _se_is_bridge(self, name): |
829 | + return name in self.data['bridges'] |
830 | + |
831 | + def _se_is_vlan(self, name): |
832 | + return name in self.data['vlans'] |
833 | + |
834 | + def _se_interface_has_own_mac(self, name): |
835 | + return name in self.data['own_macs'] |
836 | + |
837 | + def _mock_setup(self): |
838 | + self.data = copy.deepcopy(self._data) |
839 | + self.data['devices'] = set(list(self.data['macs'].keys())) |
840 | + mocks = ('get_devicelist', 'get_interface_mac', 'is_bridge', |
841 | + 'interface_has_own_mac', 'is_vlan', 'device_driver', |
842 | + 'device_devid') |
843 | + self.mocks = {} |
844 | + for n in mocks: |
845 | + m = mock.patch('cloudinit.net.' + n, |
846 | + side_effect=getattr(self, '_se_' + n)) |
847 | + self.addCleanup(m.stop) |
848 | + self.mocks[n] = m.start() |
849 | + |
850 | + def test_gi_includes_duplicate_macs(self): |
851 | + self._mock_setup() |
852 | + ret = net.get_interfaces() |
853 | + |
854 | + self.assertIn('enp0s1', self._se_get_devicelist()) |
855 | + self.assertIn('eth1', self._se_get_devicelist()) |
856 | + found = [ent for ent in ret if 'aa:aa:aa:aa:aa:01' in ent] |
857 | + self.assertEqual(len(found), 2) |
858 | + |
859 | + def test_gi_excludes_any_without_mac_address(self): |
860 | + self._mock_setup() |
861 | + ret = net.get_interfaces() |
862 | + |
863 | + self.assertIn('tun0', self._se_get_devicelist()) |
864 | + found = [ent for ent in ret if 'tun0' in ent] |
865 | + self.assertEqual(len(found), 0) |
866 | + |
867 | + def test_gi_excludes_stolen_macs(self): |
868 | + self._mock_setup() |
869 | + ret = net.get_interfaces() |
870 | + self.mocks['interface_has_own_mac'].assert_has_calls( |
871 | + [mock.call('enp0s1'), mock.call('bond1')], any_order=True) |
872 | + expected = [ |
873 | + ('enp0s2', 'aa:aa:aa:aa:aa:02', 'e1000', '0x5'), |
874 | + ('enp0s1', 'aa:aa:aa:aa:aa:01', 'virtio_net', '0x4'), |
875 | + ('eth1', 'aa:aa:aa:aa:aa:01', 'mlx4_core', '0x6'), |
876 | + ('lo', '00:00:00:00:00:00', None, '0x8'), |
877 | + ('bridge1-nic', 'aa:aa:aa:aa:aa:03', None, '0x3'), |
878 | + ] |
879 | + self.assertEqual(sorted(expected), sorted(ret)) |
880 | + |
881 | + def test_gi_excludes_bridges(self): |
882 | + self._mock_setup() |
883 | + # add a device 'b1', make all return they have their "own mac", |
884 | + # set everything other than 'b1' to be a bridge. |
885 | + # then expect b1 is the only thing left. |
886 | + self.data['macs']['b1'] = 'aa:aa:aa:aa:aa:b1' |
887 | + self.data['drivers']['b1'] = None |
888 | + self.data['devices'].add('b1') |
889 | + self.data['bonds'] = [] |
890 | + self.data['own_macs'] = self.data['devices'] |
891 | + self.data['bridges'] = [f for f in self.data['devices'] if f != "b1"] |
892 | + ret = net.get_interfaces() |
893 | + self.assertEqual([('b1', 'aa:aa:aa:aa:aa:b1', None, '0x0')], ret) |
894 | + self.mocks['is_bridge'].assert_has_calls( |
895 | + [mock.call('bridge1'), mock.call('enp0s1'), mock.call('bond1'), |
896 | + mock.call('b1')], |
897 | + any_order=True) |
898 | + |
899 | + |
900 | class TestGetInterfacesByMac(CiTestCase): |
901 | _data = {'bonds': ['bond1'], |
902 | 'bridges': ['bridge1'], |
903 | @@ -1691,4 +1941,202 @@ def _gzip_data(data): |
904 | gzfp.close() |
905 | return iobuf.getvalue() |
906 | |
907 | + |
908 | +class TestRenameInterfaces(CiTestCase): |
909 | + |
910 | + @mock.patch('cloudinit.util.subp') |
911 | + def test_rename_all(self, mock_subp): |
912 | + renames = [ |
913 | + ('00:11:22:33:44:55', 'interface0', 'virtio_net', '0x3'), |
914 | + ('00:11:22:33:44:aa', 'interface2', 'virtio_net', '0x5'), |
915 | + ] |
916 | + current_info = { |
917 | + 'ens3': { |
918 | + 'downable': True, |
919 | + 'device_id': '0x3', |
920 | + 'driver': 'virtio_net', |
921 | + 'mac': '00:11:22:33:44:55', |
922 | + 'name': 'ens3', |
923 | + 'up': False}, |
924 | + 'ens5': { |
925 | + 'downable': True, |
926 | + 'device_id': '0x5', |
927 | + 'driver': 'virtio_net', |
928 | + 'mac': '00:11:22:33:44:aa', |
929 | + 'name': 'ens5', |
930 | + 'up': False}, |
931 | + } |
932 | + net._rename_interfaces(renames, current_info=current_info) |
933 | + print(mock_subp.call_args_list) |
934 | + mock_subp.assert_has_calls([ |
935 | + mock.call(['ip', 'link', 'set', 'ens3', 'name', 'interface0'], |
936 | + capture=True), |
937 | + mock.call(['ip', 'link', 'set', 'ens5', 'name', 'interface2'], |
938 | + capture=True), |
939 | + ]) |
940 | + |
941 | + @mock.patch('cloudinit.util.subp') |
942 | + def test_rename_no_driver_no_device_id(self, mock_subp): |
943 | + renames = [ |
944 | + ('00:11:22:33:44:55', 'interface0', None, None), |
945 | + ('00:11:22:33:44:aa', 'interface1', None, None), |
946 | + ] |
947 | + current_info = { |
948 | + 'eth0': { |
949 | + 'downable': True, |
950 | + 'device_id': None, |
951 | + 'driver': None, |
952 | + 'mac': '00:11:22:33:44:55', |
953 | + 'name': 'eth0', |
954 | + 'up': False}, |
955 | + 'eth1': { |
956 | + 'downable': True, |
957 | + 'device_id': None, |
958 | + 'driver': None, |
959 | + 'mac': '00:11:22:33:44:aa', |
960 | + 'name': 'eth1', |
961 | + 'up': False}, |
962 | + } |
963 | + net._rename_interfaces(renames, current_info=current_info) |
964 | + print(mock_subp.call_args_list) |
965 | + mock_subp.assert_has_calls([ |
966 | + mock.call(['ip', 'link', 'set', 'eth0', 'name', 'interface0'], |
967 | + capture=True), |
968 | + mock.call(['ip', 'link', 'set', 'eth1', 'name', 'interface1'], |
969 | + capture=True), |
970 | + ]) |
971 | + |
972 | + @mock.patch('cloudinit.util.subp') |
973 | + def test_rename_all_bounce(self, mock_subp): |
974 | + renames = [ |
975 | + ('00:11:22:33:44:55', 'interface0', 'virtio_net', '0x3'), |
976 | + ('00:11:22:33:44:aa', 'interface2', 'virtio_net', '0x5'), |
977 | + ] |
978 | + current_info = { |
979 | + 'ens3': { |
980 | + 'downable': True, |
981 | + 'device_id': '0x3', |
982 | + 'driver': 'virtio_net', |
983 | + 'mac': '00:11:22:33:44:55', |
984 | + 'name': 'ens3', |
985 | + 'up': True}, |
986 | + 'ens5': { |
987 | + 'downable': True, |
988 | + 'device_id': '0x5', |
989 | + 'driver': 'virtio_net', |
990 | + 'mac': '00:11:22:33:44:aa', |
991 | + 'name': 'ens5', |
992 | + 'up': True}, |
993 | + } |
994 | + net._rename_interfaces(renames, current_info=current_info) |
995 | + print(mock_subp.call_args_list) |
996 | + mock_subp.assert_has_calls([ |
997 | + mock.call(['ip', 'link', 'set', 'ens3', 'down'], capture=True), |
998 | + mock.call(['ip', 'link', 'set', 'ens3', 'name', 'interface0'], |
999 | + capture=True), |
1000 | + mock.call(['ip', 'link', 'set', 'ens5', 'down'], capture=True), |
1001 | + mock.call(['ip', 'link', 'set', 'ens5', 'name', 'interface2'], |
1002 | + capture=True), |
1003 | + mock.call(['ip', 'link', 'set', 'interface0', 'up'], capture=True), |
1004 | + mock.call(['ip', 'link', 'set', 'interface2', 'up'], capture=True) |
1005 | + ]) |
1006 | + |
1007 | + @mock.patch('cloudinit.util.subp') |
1008 | + def test_rename_duplicate_macs(self, mock_subp): |
1009 | + renames = [ |
1010 | + ('00:11:22:33:44:55', 'eth0', 'hv_netsvc', '0x3'), |
1011 | + ('00:11:22:33:44:55', 'vf1', 'mlx4_core', '0x5'), |
1012 | + ] |
1013 | + current_info = { |
1014 | + 'eth0': { |
1015 | + 'downable': True, |
1016 | + 'device_id': '0x3', |
1017 | + 'driver': 'hv_netsvc', |
1018 | + 'mac': '00:11:22:33:44:55', |
1019 | + 'name': 'eth0', |
1020 | + 'up': False}, |
1021 | + 'eth1': { |
1022 | + 'downable': True, |
1023 | + 'device_id': '0x5', |
1024 | + 'driver': 'mlx4_core', |
1025 | + 'mac': '00:11:22:33:44:55', |
1026 | + 'name': 'eth1', |
1027 | + 'up': False}, |
1028 | + } |
1029 | + net._rename_interfaces(renames, current_info=current_info) |
1030 | + print(mock_subp.call_args_list) |
1031 | + mock_subp.assert_has_calls([ |
1032 | + mock.call(['ip', 'link', 'set', 'eth1', 'name', 'vf1'], |
1033 | + capture=True), |
1034 | + ]) |
1035 | + |
1036 | + @mock.patch('cloudinit.util.subp') |
1037 | + def test_rename_duplicate_macs_driver_no_devid(self, mock_subp): |
1038 | + renames = [ |
1039 | + ('00:11:22:33:44:55', 'eth0', 'hv_netsvc', None), |
1040 | + ('00:11:22:33:44:55', 'vf1', 'mlx4_core', None), |
1041 | + ] |
1042 | + current_info = { |
1043 | + 'eth0': { |
1044 | + 'downable': True, |
1045 | + 'device_id': '0x3', |
1046 | + 'driver': 'hv_netsvc', |
1047 | + 'mac': '00:11:22:33:44:55', |
1048 | + 'name': 'eth0', |
1049 | + 'up': False}, |
1050 | + 'eth1': { |
1051 | + 'downable': True, |
1052 | + 'device_id': '0x5', |
1053 | + 'driver': 'mlx4_core', |
1054 | + 'mac': '00:11:22:33:44:55', |
1055 | + 'name': 'eth1', |
1056 | + 'up': False}, |
1057 | + } |
1058 | + net._rename_interfaces(renames, current_info=current_info) |
1059 | + print(mock_subp.call_args_list) |
1060 | + mock_subp.assert_has_calls([ |
1061 | + mock.call(['ip', 'link', 'set', 'eth1', 'name', 'vf1'], |
1062 | + capture=True), |
1063 | + ]) |
1064 | + |
1065 | + @mock.patch('cloudinit.util.subp') |
1066 | + def test_rename_multi_mac_dups(self, mock_subp): |
1067 | + renames = [ |
1068 | + ('00:11:22:33:44:55', 'eth0', 'hv_netsvc', '0x3'), |
1069 | + ('00:11:22:33:44:55', 'vf1', 'mlx4_core', '0x5'), |
1070 | + ('00:11:22:33:44:55', 'vf2', 'mlx4_core', '0x7'), |
1071 | + ] |
1072 | + current_info = { |
1073 | + 'eth0': { |
1074 | + 'downable': True, |
1075 | + 'device_id': '0x3', |
1076 | + 'driver': 'hv_netsvc', |
1077 | + 'mac': '00:11:22:33:44:55', |
1078 | + 'name': 'eth0', |
1079 | + 'up': False}, |
1080 | + 'eth1': { |
1081 | + 'downable': True, |
1082 | + 'device_id': '0x5', |
1083 | + 'driver': 'mlx4_core', |
1084 | + 'mac': '00:11:22:33:44:55', |
1085 | + 'name': 'eth1', |
1086 | + 'up': False}, |
1087 | + 'eth2': { |
1088 | + 'downable': True, |
1089 | + 'device_id': '0x7', |
1090 | + 'driver': 'mlx4_core', |
1091 | + 'mac': '00:11:22:33:44:55', |
1092 | + 'name': 'eth2', |
1093 | + 'up': False}, |
1094 | + } |
1095 | + net._rename_interfaces(renames, current_info=current_info) |
1096 | + print(mock_subp.call_args_list) |
1097 | + mock_subp.assert_has_calls([ |
1098 | + mock.call(['ip', 'link', 'set', 'eth1', 'name', 'vf1'], |
1099 | + capture=True), |
1100 | + mock.call(['ip', 'link', 'set', 'eth2', 'name', 'vf2'], |
1101 | + capture=True), |
1102 | + ]) |
1103 | + |
1104 | + |
1105 | # vi: ts=4 expandtab |
There are some comments in line. Others here.
1. I'd have thought that there would need to be a fix for the 'duplicate mac address detected' as we see /bugs.launchpad .net/ubuntu/ +source/ cloud-init/ +bug/1692028
https:/
I'm not sure how that is being avoided. It seems even if we don't call 'get_interfaces _by_mac( )' in the proposed path for azure, we are likely to end up calling that function at some point in some generic code path and on azure where there might be duplicate macs, it will fail.
A solution would be to change callers i guess to accept that there *might* be identical macs.
B. It seems like there is duplicate code in DataSourceAzureNet and DataSourceAzure. I'd like to have just one class, i think we should be able to do that. I suspect that might be just work-in-progress.
C. It seems to me that you've added support for Azure being able to provide config that includes a device driver (in 'network config v1 format'). But generically, I do not believe that is present in v1 or v2/netplan format. If that *is* true, then there isn't actually a way that those formats can specify this configuration, and we should at least open issues to that affect.
D. network_config will still only be applied once per instance. So If a user shuts down an instance and attaches a device and starts it back up, I do not think we will generate a different/updated config. Is that right? Its likely i'm not understanding something.
E. When testing this please make sure you test: cloud/instance/ obj.pkl that references DataSourceAzureNet. cloud-init* and cloud_config entries in /etc/fstab. (mocking a clean instance)
i.) test upgrade and reboot on same instance. This will test the path where there exists a /var/lib/
ii.) test upgrade and reboot with clean of /var/log/cloud* /var/lib/