Merge ~raharper/cloud-init:azure_run_local into cloud-init:master

Proposed by Ryan Harper
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)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Scott Moser Pending
Review via email: mp+326099@code.launchpad.net

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

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) 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://bugs.launchpad.net/ubuntu/+source/cloud-init/+bug/1692028

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:
  i.) test upgrade and reboot on same instance. This will test the path where there exists a /var/lib/cloud/instance/obj.pkl that references DataSourceAzureNet.
  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)

Revision history for this message
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).

1. http://paste.ubuntu.com/24919297/

Revision history for this message
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://bugs.launchpad.net/ubuntu/+source/cloud-init/+bug/1692028

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_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.

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,mac,driver) but want a rule that sets NAME=vf%k so it can be parameterized.

>
> 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/cloud/instance/obj.pkl that references
> 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.

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

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

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

review: Needs Fixing (continuous-integration)
~raharper/cloud-init:azure_run_local updated
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

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

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

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

review: Needs Fixing (continuous-integration)
~raharper/cloud-init:azure_run_local updated
5105963... by Ryan Harper

Don't allow bonds into fallback network-config

b0b3257... by Ryan Harper

flake8 fixes

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

PASSED: Continuous integration, rev:b0b325796718ce04393472399b9f60d913a3aaa8
https://jenkins.ubuntu.com/server/job/cloud-init-ci/9/
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://jenkins.ubuntu.com/server/job/cloud-init-ci/9/rebuild

review: Approve (continuous-integration)
~raharper/cloud-init:azure_run_local updated
f31c8a0... by Ryan Harper

Disable azure bonding fallback config generation.

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

PASSED: Continuous integration, rev:f31c8a02f75fcb1aeafe665dc6b240a0ff542a05
https://jenkins.ubuntu.com/server/job/cloud-init-ci/10/
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://jenkins.ubuntu.com/server/job/cloud-init-ci/10/rebuild

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) :
Revision history for this message
Ryan Harper (raharper) wrote :
Download full text (7.8 KiB)

On Mon, Jun 26, 2017 at 2:11 PM, Scott Moser <email address hidden> wrote:

>
>
> Diff comments:
>
> > diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
> > index 65accbb..7e3333f 100644
> > --- a/cloudinit/net/__init__.py
> > +++ b/cloudinit/net/__init__.py
> > @@ -124,6 +128,26 @@ def is_present(devname):
> > return os.path.exists(sys_dev_path(devname))
> >
> >
> > +def device_driver(devname):
> > + """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_path(devname, "device/driver")
> > + # driver is a symlink to the driver *dir*
> > + if os.path.islink(driver_path):
> > + driver = os.path.basename(os.readlink(driver_path))
> > +
> > + return driver
> > +
> > +
> > +def device_devid(devname):
> > + """Return the device id string for net device named 'devname' """
> > + dev_id = read_sys_net_safe(devname, "device/device")
> > + if dev_id is False:
> > + return None
> > +
> > + return dev_id
> > +
> > +
> > def get_devicelist():
> > return os.listdir(SYS_CLASS_NET)
> >
> > @@ -304,14 +367,44 @@ def _rename_interfaces(renames,
> strict_present=True, strict_busy=True,
> > ops = []
> > errors = []
> > ups = []
> > - cur_byname = update_byname(cur_bymac)
> > + cur_byname = update_byname(cur_info)
> > 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...

Read more...

~raharper/cloud-init:azure_run_local updated
1cedcbc... by Ryan Harper

Fix docstring punctuation. Raise exception on matching multiple devices

df7874a... by Ryan Harper

Fix spelling errors in comment block.

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

Updated with fixes for ACK'ed issues. Let's decide what to do with get_interfaces_by_mac; I suggest dropping it altogether since by-mac path will fail on duplicate macs.

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

PASSED: Continuous integration, rev:df7874a9ecee918cceacecdbbc5c2988380849a1
https://jenkins.ubuntu.com/server/job/cloud-init-ci/12/
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://jenkins.ubuntu.com/server/job/cloud-init-ci/12/rebuild

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

I have to fix some test issues but
i think my branch at
 https://code.launchpad.net/~smoser/cloud-init/+git/cloud-init/+ref/azure_run_local
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 d87ba732d966fdb63183e66b39411eccd82a0146 seems to work ok.

I'll continue to work from there tomorrow.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index 65accbb..cba991a 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -97,6 +97,10 @@ def is_bridge(devname):
97 return os.path.exists(sys_dev_path(devname, "bridge"))97 return os.path.exists(sys_dev_path(devname, "bridge"))
9898
9999
100def is_bond(devname):
101 return os.path.exists(sys_dev_path(devname, "bonding"))
102
103
100def is_vlan(devname):104def is_vlan(devname):
101 uevent = str(read_sys_net_safe(devname, "uevent"))105 uevent = str(read_sys_net_safe(devname, "uevent"))
102 return 'DEVTYPE=vlan' in uevent.splitlines()106 return 'DEVTYPE=vlan' in uevent.splitlines()
@@ -124,6 +128,26 @@ def is_present(devname):
124 return os.path.exists(sys_dev_path(devname))128 return os.path.exists(sys_dev_path(devname))
125129
126130
131def device_driver(devname):
132 """Return the device driver for net device named 'devname'."""
133 driver = None
134 driver_path = sys_dev_path(devname, "device/driver")
135 # driver is a symlink to the driver *dir*
136 if os.path.islink(driver_path):
137 driver = os.path.basename(os.readlink(driver_path))
138
139 return driver
140
141
142def device_devid(devname):
143 """Return the device id string for net device named 'devname'."""
144 dev_id = read_sys_net_safe(devname, "device/device")
145 if dev_id is False:
146 return None
147
148 return dev_id
149
150
127def get_devicelist():151def get_devicelist():
128 return os.listdir(SYS_CLASS_NET)152 return os.listdir(SYS_CLASS_NET)
129153
@@ -138,12 +162,21 @@ def is_disabled_cfg(cfg):
138 return cfg.get('config') == "disabled"162 return cfg.get('config') == "disabled"
139163
140164
141def generate_fallback_config():165def generate_fallback_config(blacklist_drivers=None, config_driver=None):
142 """Determine which attached net dev is most likely to have a connection and166 """Determine which attached net dev is most likely to have a connection and
143 generate network state to run dhcp on that interface"""167 generate network state to run dhcp on that interface"""
168
169 if not config_driver:
170 config_driver = False
171
172 if not blacklist_drivers:
173 blacklist_drivers = []
174
144 # get list of interfaces that could have connections175 # get list of interfaces that could have connections
145 invalid_interfaces = set(['lo'])176 invalid_interfaces = set(['lo'])
146 potential_interfaces = set(get_devicelist())177 potential_interfaces = set([device for device in get_devicelist()
178 if device_driver(device) not in
179 blacklist_drivers])
147 potential_interfaces = potential_interfaces.difference(invalid_interfaces)180 potential_interfaces = potential_interfaces.difference(invalid_interfaces)
148 # sort into interfaces with carrier, interfaces which could have carrier,181 # sort into interfaces with carrier, interfaces which could have carrier,
149 # and ignore interfaces that are definitely disconnected182 # and ignore interfaces that are definitely disconnected
@@ -155,6 +188,9 @@ def generate_fallback_config():
155 if is_bridge(interface):188 if is_bridge(interface):
156 # skip any bridges189 # skip any bridges
157 continue190 continue
191 if is_bond(interface):
192 # skip any bonds
193 continue
158 carrier = read_sys_net_int(interface, 'carrier')194 carrier = read_sys_net_int(interface, 'carrier')
159 if carrier:195 if carrier:
160 connected.append(interface)196 connected.append(interface)
@@ -194,9 +230,18 @@ def generate_fallback_config():
194 break230 break
195 if target_mac and target_name:231 if target_mac and target_name:
196 nconf = {'config': [], 'version': 1}232 nconf = {'config': [], 'version': 1}
197 nconf['config'].append(233 cfg = {'type': 'physical', 'name': target_name,
198 {'type': 'physical', 'name': target_name,234 'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]}
199 'mac_address': target_mac, 'subnets': [{'type': 'dhcp'}]})235 # inject the device driver name, dev_id into config if enabled and
236 # device has a valid device driver value
237 if config_driver:
238 driver = device_driver(target_name)
239 if driver:
240 cfg['params'] = {
241 'driver': driver,
242 'device_id': device_devid(target_name),
243 }
244 nconf['config'].append(cfg)
200 return nconf245 return nconf
201 else:246 else:
202 # can't read any interfaces addresses (or there are none); give up247 # can't read any interfaces addresses (or there are none); give up
@@ -217,10 +262,16 @@ def apply_network_config_names(netcfg, strict_present=True, strict_busy=True):
217 if ent.get('type') != 'physical':262 if ent.get('type') != 'physical':
218 continue263 continue
219 mac = ent.get('mac_address')264 mac = ent.get('mac_address')
220 name = ent.get('name')
221 if not mac:265 if not mac:
222 continue266 continue
223 renames.append([mac, name])267 name = ent.get('name')
268 driver = ent.get('params', {}).get('driver')
269 device_id = ent.get('params', {}).get('device_id')
270 if not driver:
271 driver = device_driver(name)
272 if not device_id:
273 device_id = device_devid(name)
274 renames.append([mac, name, driver, device_id])
224275
225 return _rename_interfaces(renames)276 return _rename_interfaces(renames)
226277
@@ -245,15 +296,27 @@ def _get_current_rename_info(check_downable=True):
245 """Collect information necessary for rename_interfaces.296 """Collect information necessary for rename_interfaces.
246297
247 returns a dictionary by mac address like:298 returns a dictionary by mac address like:
248 {mac:299 {name:
249 {'name': name300 {
250 'up': boolean: is_up(name),
251 'downable': None or boolean indicating that the301 'downable': None or boolean indicating that the
252 device has only automatically assigned ip addrs.}}302 device has only automatically assigned ip addrs.
303 'device_id': Device id value (if it has one)
304 'driver': Device driver (if it has one)
305 'mac': mac address
306 'name': name
307 'up': boolean: is_up(name)
308 }}
253 """309 """
254 bymac = {}310 cur_info = {}
255 for mac, name in get_interfaces_by_mac().items():311 for (name, mac, driver, device_id) in get_interfaces():
256 bymac[mac] = {'name': name, 'up': is_up(name), 'downable': None}312 cur_info[name] = {
313 'downable': None,
314 'device_id': device_id,
315 'driver': driver,
316 'mac': mac,
317 'name': name,
318 'up': is_up(name),
319 }
257320
258 if check_downable:321 if check_downable:
259 nmatch = re.compile(r"[0-9]+:\s+(\w+)[@:]")322 nmatch = re.compile(r"[0-9]+:\s+(\w+)[@:]")
@@ -265,11 +328,11 @@ def _get_current_rename_info(check_downable=True):
265 for bytes_out in (ipv6, ipv4):328 for bytes_out in (ipv6, ipv4):
266 nics_with_addresses.update(nmatch.findall(bytes_out))329 nics_with_addresses.update(nmatch.findall(bytes_out))
267330
268 for d in bymac.values():331 for d in cur_info.values():
269 d['downable'] = (d['up'] is False or332 d['downable'] = (d['up'] is False or
270 d['name'] not in nics_with_addresses)333 d['name'] not in nics_with_addresses)
271334
272 return bymac335 return cur_info
273336
274337
275def _rename_interfaces(renames, strict_present=True, strict_busy=True,338def _rename_interfaces(renames, strict_present=True, strict_busy=True,
@@ -282,15 +345,15 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True,
282 if current_info is None:345 if current_info is None:
283 current_info = _get_current_rename_info()346 current_info = _get_current_rename_info()
284347
285 cur_bymac = {}348 cur_info = {}
286 for mac, data in current_info.items():349 for name, data in current_info.items():
287 cur = data.copy()350 cur = data.copy()
288 cur['mac'] = mac351 cur['name'] = name
289 cur_bymac[mac] = cur352 cur_info[name] = cur
290353
291 def update_byname(bymac):354 def update_byname(bymac):
292 return dict((data['name'], data)355 return dict((data['name'], data)
293 for data in bymac.values())356 for data in cur_info.values())
294357
295 def rename(cur, new):358 def rename(cur, new):
296 util.subp(["ip", "link", "set", cur, "name", new], capture=True)359 util.subp(["ip", "link", "set", cur, "name", new], capture=True)
@@ -304,14 +367,48 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True,
304 ops = []367 ops = []
305 errors = []368 errors = []
306 ups = []369 ups = []
307 cur_byname = update_byname(cur_bymac)370 cur_byname = update_byname(cur_info)
308 tmpname_fmt = "cirename%d"371 tmpname_fmt = "cirename%d"
309 tmpi = -1372 tmpi = -1
310373
311 for mac, new_name in renames:374 def entry_match(data, mac, driver, device_id):
312 cur = cur_bymac.get(mac, {})375 """match if set and in data"""
313 cur_name = cur.get('name')376 if mac and driver and device_id:
377 return (data['mac'] == mac and
378 data['driver'] == driver and
379 data['device_id'] == device_id)
380 elif mac and driver:
381 return (data['mac'] == mac and
382 data['driver'] == driver)
383 elif mac:
384 return (data['mac'] == mac)
385
386 return False
387
388 def find_entry(mac, driver, device_id):
389 match = [data for data in cur_info.values()
390 if entry_match(data, mac, driver, device_id)]
391 if len(match):
392 if len(match) > 1:
393 msg = ('Failed to match a single device. Matched devices "%s"'
394 ' with search values "(mac:%s driver:%s device_id:%s)"'
395 % (match, mac, driver, device_id))
396 raise ValueError(msg)
397 return match[0]
398
399 return None
400
401 for mac, new_name, driver, device_id in renames:
314 cur_ops = []402 cur_ops = []
403 cur = find_entry(mac, driver, device_id)
404 if not cur:
405 if strict_present:
406 errors.append(
407 "[nic not present] Cannot rename mac=%s to %s"
408 ", not available." % (mac, new_name))
409 continue
410
411 cur_name = cur.get('name')
315 if cur_name == new_name:412 if cur_name == new_name:
316 # nothing to do413 # nothing to do
317 continue414 continue
@@ -351,13 +448,13 @@ def _rename_interfaces(renames, strict_present=True, strict_busy=True,
351448
352 cur_ops.append(("rename", mac, new_name, (new_name, tmp_name)))449 cur_ops.append(("rename", mac, new_name, (new_name, tmp_name)))
353 target['name'] = tmp_name450 target['name'] = tmp_name
354 cur_byname = update_byname(cur_bymac)451 cur_byname = update_byname(cur_info)
355 if target['up']:452 if target['up']:
356 ups.append(("up", mac, new_name, (tmp_name,)))453 ups.append(("up", mac, new_name, (tmp_name,)))
357454
358 cur_ops.append(("rename", mac, new_name, (cur['name'], new_name)))455 cur_ops.append(("rename", mac, new_name, (cur['name'], new_name)))
359 cur['name'] = new_name456 cur['name'] = new_name
360 cur_byname = update_byname(cur_bymac)457 cur_byname = update_byname(cur_info)
361 ops += cur_ops458 ops += cur_ops
362459
363 opmap = {'rename': rename, 'down': down, 'up': up}460 opmap = {'rename': rename, 'down': down, 'up': up}
@@ -426,6 +523,36 @@ def get_interfaces_by_mac():
426 return ret523 return ret
427524
428525
526def get_interfaces():
527 """Return list of interface tuples (name, mac, driver, device_id)
528
529 Bridges and any devices that have a 'stolen' mac are excluded."""
530 try:
531 devs = get_devicelist()
532 except OSError as e:
533 if e.errno == errno.ENOENT:
534 devs = []
535 else:
536 raise
537 ret = []
538 empty_mac = '00:00:00:00:00:00'
539 for name in devs:
540 if not interface_has_own_mac(name):
541 continue
542 if is_bridge(name):
543 continue
544 if is_vlan(name):
545 continue
546 mac = get_interface_mac(name)
547 # some devices may not have a mac (tun0)
548 if not mac:
549 continue
550 if mac == empty_mac and name != 'lo':
551 continue
552 ret.append((name, mac, device_driver(name), device_devid(name)))
553 return ret
554
555
429class RendererNotFoundError(RuntimeError):556class RendererNotFoundError(RuntimeError):
430 pass557 pass
431558
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
index 98ce01e..b707146 100644
--- a/cloudinit/net/eni.py
+++ b/cloudinit/net/eni.py
@@ -72,6 +72,8 @@ def _iface_add_attrs(iface, index):
72 content = []72 content = []
73 ignore_map = [73 ignore_map = [
74 'control',74 'control',
75 'device_id',
76 'driver',
75 'index',77 'index',
76 'inet',78 'inet',
77 'mode',79 'mode',
diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py
index c68658d..bba139e 100644
--- a/cloudinit/net/renderer.py
+++ b/cloudinit/net/renderer.py
@@ -34,8 +34,10 @@ class Renderer(object):
34 for iface in network_state.iter_interfaces(filter_by_physical):34 for iface in network_state.iter_interfaces(filter_by_physical):
35 # for physical interfaces write out a persist net udev rule35 # for physical interfaces write out a persist net udev rule
36 if 'name' in iface and iface.get('mac_address'):36 if 'name' in iface and iface.get('mac_address'):
37 driver = iface.get('driver', None)
37 content.write(generate_udev_rule(iface['name'],38 content.write(generate_udev_rule(iface['name'],
38 iface['mac_address']))39 iface['mac_address'],
40 driver=driver))
39 return content.getvalue()41 return content.getvalue()
4042
41 @abc.abstractmethod43 @abc.abstractmethod
diff --git a/cloudinit/net/udev.py b/cloudinit/net/udev.py
index fd2fd8c..58c0a70 100644
--- a/cloudinit/net/udev.py
+++ b/cloudinit/net/udev.py
@@ -23,7 +23,7 @@ def compose_udev_setting(key, value):
23 return '%s="%s"' % (key, value)23 return '%s="%s"' % (key, value)
2424
2525
26def generate_udev_rule(interface, mac):26def generate_udev_rule(interface, mac, driver=None):
27 """Return a udev rule to set the name of network interface with `mac`.27 """Return a udev rule to set the name of network interface with `mac`.
2828
29 The rule ends up as a single line looking something like:29 The rule ends up as a single line looking something like:
@@ -31,10 +31,13 @@ def generate_udev_rule(interface, mac):
31 SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*",31 SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*",
32 ATTR{address}="ff:ee:dd:cc:bb:aa", NAME="eth0"32 ATTR{address}="ff:ee:dd:cc:bb:aa", NAME="eth0"
33 """33 """
34 if not driver:
35 driver = '?*'
36
34 rule = ', '.join([37 rule = ', '.join([
35 compose_udev_equality('SUBSYSTEM', 'net'),38 compose_udev_equality('SUBSYSTEM', 'net'),
36 compose_udev_equality('ACTION', 'add'),39 compose_udev_equality('ACTION', 'add'),
37 compose_udev_equality('DRIVERS', '?*'),40 compose_udev_equality('DRIVERS', driver),
38 compose_udev_attr_equality('address', mac),41 compose_udev_attr_equality('address', mac),
39 compose_udev_setting('NAME', interface),42 compose_udev_setting('NAME', interface),
40 ])43 ])
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 4fe0d63..13ec5a0 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -16,6 +16,7 @@ from xml.dom import minidom
16import xml.etree.ElementTree as ET16import xml.etree.ElementTree as ET
1717
18from cloudinit import log as logging18from cloudinit import log as logging
19from cloudinit import net
19from cloudinit import sources20from cloudinit import sources
20from cloudinit.sources.helpers.azure import get_metadata_from_fabric21from cloudinit.sources.helpers.azure import get_metadata_from_fabric
21from cloudinit import util22from cloudinit import util
@@ -255,6 +256,7 @@ class DataSourceAzureNet(sources.DataSource):
255 util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}),256 util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}),
256 BUILTIN_DS_CONFIG])257 BUILTIN_DS_CONFIG])
257 self.dhclient_lease_file = self.ds_cfg.get('dhclient_lease_file')258 self.dhclient_lease_file = self.ds_cfg.get('dhclient_lease_file')
259 self._network_config = None
258260
259 def __str__(self):261 def __str__(self):
260 root = sources.DataSource.__str__(self)262 root = sources.DataSource.__str__(self)
@@ -331,6 +333,14 @@ class DataSourceAzureNet(sources.DataSource):
331 if asset_tag != AZURE_CHASSIS_ASSET_TAG:333 if asset_tag != AZURE_CHASSIS_ASSET_TAG:
332 LOG.debug("Non-Azure DMI asset tag '%s' discovered.", asset_tag)334 LOG.debug("Non-Azure DMI asset tag '%s' discovered.", asset_tag)
333 return False335 return False
336
337 self.dsmode = self._determine_dsmode(get_dsmodes(self._network_config))
338 if self.dsmode == sources.DSMODE_LOCAL:
339 self.metadata['instance-id'] = util.read_dmi_data('system-uuid')
340 # switch dsmode to network so init-net can run as well
341 self.dsmode = sources.DSMODE_NETWORK
342 return True
343
334 ddir = self.ds_cfg['data_dir']344 ddir = self.ds_cfg['data_dir']
335345
336 candidates = [self.seed_dir]346 candidates = [self.seed_dir]
@@ -423,6 +433,50 @@ class DataSourceAzureNet(sources.DataSource):
423 address_ephemeral_resize(is_new_instance=is_new_instance)433 address_ephemeral_resize(is_new_instance=is_new_instance)
424 return434 return
425435
436 @property
437 def network_config(self):
438 """Generate a network config like net.generate_fallback_network() with
439 the following execptions.
440
441 1. Probe the drivers of the net-devices present and inject them in
442 the network configuration under params: driver: <driver> value
443 2. If the driver value is 'mlx4_core', the control mode should be
444 set to manual. The device will be later used to build a bond,
445 for now we want to ensure the device gets named but does not
446 break any network configuration
447 """
448 blacklist = ['mlx4_core']
449 if not self._network_config:
450 LOG.debug('Azure: generating fallback configuration')
451 # generate a network config, blacklist picking any mlx4_core devs
452 netconfig = net.generate_fallback_config(
453 blacklist_drivers=blacklist, config_driver=True)
454
455 # if we have any blacklisted devices, update the network_config to
456 # include the device, mac, and driver values, but with no ip
457 # config; this ensures udev rules are generated but won't affect
458 # ip configuration
459 bl_found = 0
460 for bl_dev in [dev for dev in net.get_devicelist()
461 if net.device_driver(dev) in blacklist]:
462 bl_found += 1
463 cfg = {
464 'type': 'physical',
465 'name': 'vf%d' % bl_found,
466 'mac_address': net.get_interface_mac(bl_dev),
467 'params': {
468 'driver': net.device_driver(bl_dev),
469 'device_id': net.device_devid(bl_dev),
470 },
471 }
472 netconfig['config'].append(cfg)
473
474 # switch to network mode after generating a config
475 self.dsmode = sources.DSMODE_NETWORK
476 self._network_config = netconfig
477
478 return self._network_config
479
426480
427def _partitions_on_device(devpath, maxnum=16):481def _partitions_on_device(devpath, maxnum=16):
428 # return a list of tuples (ptnum, path) for each part on devpath482 # return a list of tuples (ptnum, path) for each part on devpath
@@ -641,6 +695,12 @@ def invoke_agent(cmd):
641 LOG.debug("not invoking agent")695 LOG.debug("not invoking agent")
642696
643697
698def get_dsmodes(netconfig=None):
699 """Return which dsmode we use be in based on current network config"""
700 return [sources.DSMODE_LOCAL
701 if not netconfig else sources.DSMODE_NETWORK]
702
703
644def find_child(node, filter_func):704def find_child(node, filter_func):
645 ret = []705 ret = []
646 if not node.hasChildNodes():706 if not node.hasChildNodes():
@@ -851,6 +911,7 @@ class NonAzureDataSource(Exception):
851911
852# Used to match classes to dependencies912# Used to match classes to dependencies
853datasources = [913datasources = [
914 (DataSourceAzureNet, (sources.DEP_FILESYSTEM, )),
854 (DataSourceAzureNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),915 (DataSourceAzureNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
855]916]
856917
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index 7d33daf..66f9d77 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -142,6 +142,10 @@ scbus-1 on xpt0 bus 0
142 def _invoke_agent(cmd):142 def _invoke_agent(cmd):
143 data['agent_invoked'] = cmd143 data['agent_invoked'] = cmd
144144
145 def _get_dsmodes(config):
146 # sources.DSMODE_NETWORK
147 return 'net'
148
145 def _wait_for_files(flist, _maxwait=None, _naplen=None):149 def _wait_for_files(flist, _maxwait=None, _naplen=None):
146 data['waited'] = flist150 data['waited'] = flist
147 return []151 return []
@@ -171,6 +175,7 @@ scbus-1 on xpt0 bus 0
171 self.apply_patches([175 self.apply_patches([
172 (dsaz, 'list_possible_azure_ds_devs', dsdevs),176 (dsaz, 'list_possible_azure_ds_devs', dsdevs),
173 (dsaz, 'invoke_agent', _invoke_agent),177 (dsaz, 'invoke_agent', _invoke_agent),
178 (dsaz, 'get_dsmodes', _get_dsmodes),
174 (dsaz, 'wait_for_files', _wait_for_files),179 (dsaz, 'wait_for_files', _wait_for_files),
175 (dsaz, 'pubkeys_from_crt_files', _pubkeys_from_crt_files),180 (dsaz, 'pubkeys_from_crt_files', _pubkeys_from_crt_files),
176 (dsaz, 'perform_hostname_bounce', mock.MagicMock()),181 (dsaz, 'perform_hostname_bounce', mock.MagicMock()),
@@ -554,6 +559,84 @@ fdescfs /dev/fd fdescfs rw 0 0
554 self.assertEqual(559 self.assertEqual(
555 [mock.call("/dev/cd0")], m_check_fbsd_cdrom.call_args_list)560 [mock.call("/dev/cd0")], m_check_fbsd_cdrom.call_args_list)
556561
562 @mock.patch('cloudinit.net.get_interface_mac')
563 @mock.patch('cloudinit.net.get_devicelist')
564 @mock.patch('cloudinit.net.device_driver')
565 @mock.patch('cloudinit.net.generate_fallback_config')
566 def test_network_config(self, mock_fallback, mock_dd,
567 mock_devlist, mock_get_mac):
568 odata = {'HostName': "myhost", 'UserName': "myuser"}
569 data = {'ovfcontent': construct_valid_ovf_env(data=odata),
570 'sys_cfg': {}}
571
572 fallback_config = {
573 'version': 1,
574 'config': [{
575 'type': 'physical', 'name': 'eth0',
576 'mac_address': '00:11:22:33:44:55',
577 'params': {'driver': 'hv_netsvc'},
578 'subnets': [{'type': 'dhcp'}],
579 }]
580 }
581 mock_fallback.return_value = fallback_config
582
583 mock_devlist.return_value = ['eth0']
584 mock_dd.return_value = ['hv_netsvc']
585 mock_get_mac.return_value = '00:11:22:33:44:55'
586
587 dsrc = self._get_ds(data)
588 ret = dsrc.get_data()
589 self.assertTrue(ret)
590
591 netconfig = dsrc.network_config
592 self.assertEqual(netconfig, fallback_config)
593 mock_fallback.assert_called_with(blacklist_drivers=['mlx4_core'],
594 config_driver=True)
595
596 @mock.patch('cloudinit.net.get_interface_mac')
597 @mock.patch('cloudinit.net.get_devicelist')
598 @mock.patch('cloudinit.net.device_driver')
599 @mock.patch('cloudinit.net.generate_fallback_config')
600 def test_network_config_blacklist(self, mock_fallback, mock_dd,
601 mock_devlist, mock_get_mac):
602 odata = {'HostName': "myhost", 'UserName': "myuser"}
603 data = {'ovfcontent': construct_valid_ovf_env(data=odata),
604 'sys_cfg': {}}
605
606 fallback_config = {
607 'version': 1,
608 'config': [{
609 'type': 'physical', 'name': 'eth0',
610 'mac_address': '00:11:22:33:44:55',
611 'params': {'driver': 'hv_netsvc'},
612 'subnets': [{'type': 'dhcp'}],
613 }]
614 }
615 blacklist_config = {
616 'type': 'physical',
617 'name': 'eth1',
618 'mac_address': '00:11:22:33:44:55',
619 'params': {'driver': 'mlx4_core'}
620 }
621 mock_fallback.return_value = fallback_config
622
623 mock_devlist.return_value = ['eth0', 'eth1']
624 mock_dd.side_effect = [
625 'hv_netsvc', # list composition, skipped
626 'mlx4_core', # list composition, match
627 'mlx4_core', # config get driver name
628 ]
629 mock_get_mac.return_value = '00:11:22:33:44:55'
630
631 dsrc = self._get_ds(data)
632 ret = dsrc.get_data()
633 self.assertTrue(ret)
634
635 netconfig = dsrc.network_config
636 expected_config = fallback_config
637 expected_config['config'].append(blacklist_config)
638 self.assertEqual(netconfig, expected_config)
639
557640
558class TestAzureBounce(TestCase):641class TestAzureBounce(TestCase):
559642
@@ -568,6 +651,9 @@ class TestAzureBounce(TestCase):
568 self.patches.enter_context(651 self.patches.enter_context(
569 mock.patch.object(dsaz, 'get_metadata_from_fabric',652 mock.patch.object(dsaz, 'get_metadata_from_fabric',
570 mock.MagicMock(return_value={})))653 mock.MagicMock(return_value={})))
654 self.patches.enter_context(
655 mock.patch.object(dsaz, 'get_dsmodes',
656 mock.MagicMock(return_value='net')))
571657
572 def _dmi_mocks(key):658 def _dmi_mocks(key):
573 if key == 'system-uuid':659 if key == 'system-uuid':
diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/test_datasource/test_common.py
index 7649b9a..be60781 100644
--- a/tests/unittests/test_datasource/test_common.py
+++ b/tests/unittests/test_datasource/test_common.py
@@ -26,6 +26,7 @@ from cloudinit.sources import DataSourceNone as DSNone
26from .. import helpers as test_helpers26from .. import helpers as test_helpers
2727
28DEFAULT_LOCAL = [28DEFAULT_LOCAL = [
29 Azure.DataSourceAzureNet,
29 CloudSigma.DataSourceCloudSigma,30 CloudSigma.DataSourceCloudSigma,
30 ConfigDrive.DataSourceConfigDrive,31 ConfigDrive.DataSourceConfigDrive,
31 DigitalOcean.DataSourceDigitalOcean,32 DigitalOcean.DataSourceDigitalOcean,
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 8edc0b8..06e8f09 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -836,38 +836,176 @@ CONFIG_V1_EXPLICIT_LOOPBACK = {
836 'subnets': [{'control': 'auto', 'type': 'loopback'}]},836 'subnets': [{'control': 'auto', 'type': 'loopback'}]},
837 ]}837 ]}
838838
839DEFAULT_DEV_ATTRS = {
840 'eth1000': {
841 "bridge": False,
842 "carrier": False,
843 "dormant": False,
844 "operstate": "down",
845 "address": "07-1C-C6-75-A4-BE",
846 "device/driver": None,
847 "device/device": None,
848 }
849}
850
839851
840def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net,852def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net,
841 mock_sys_dev_path):853 mock_sys_dev_path, dev_attrs=None):
842 mock_get_devicelist.return_value = ['eth1000']854 if not dev_attrs:
843 dev_characteristics = {855 dev_attrs = DEFAULT_DEV_ATTRS
844 'eth1000': {856
845 "bridge": False,857 mock_get_devicelist.return_value = dev_attrs.keys()
846 "carrier": False,
847 "dormant": False,
848 "operstate": "down",
849 "address": "07-1C-C6-75-A4-BE",
850 }
851 }
852858
853 def fake_read(devname, path, translate=None,859 def fake_read(devname, path, translate=None,
854 on_enoent=None, on_keyerror=None,860 on_enoent=None, on_keyerror=None,
855 on_einval=None):861 on_einval=None):
856 return dev_characteristics[devname][path]862 return dev_attrs[devname][path]
857863
858 mock_read_sys_net.side_effect = fake_read864 mock_read_sys_net.side_effect = fake_read
859865
860 def sys_dev_path(devname, path=""):866 def sys_dev_path(devname, path=""):
861 return tmp_dir + devname + "/" + path867 return tmp_dir + "/" + devname + "/" + path
862868
863 for dev in dev_characteristics:869 for dev in dev_attrs:
864 os.makedirs(os.path.join(tmp_dir, dev))870 os.makedirs(os.path.join(tmp_dir, dev))
865 with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh:871 with open(os.path.join(tmp_dir, dev, 'operstate'), 'w') as fh:
866 fh.write("down")872 fh.write(dev_attrs[dev]['operstate'])
873 os.makedirs(os.path.join(tmp_dir, dev, "device"))
874 for key in ['device/driver']:
875 if key in dev_attrs[dev] and dev_attrs[dev][key]:
876 target = dev_attrs[dev][key]
877 link = os.path.join(tmp_dir, dev, key)
878 print('symlink %s -> %s' % (link, target))
879 os.symlink(target, link)
867880
868 mock_sys_dev_path.side_effect = sys_dev_path881 mock_sys_dev_path.side_effect = sys_dev_path
869882
870883
884class TestGenerateFallbackConfig(CiTestCase):
885
886 @mock.patch("cloudinit.net.sys_dev_path")
887 @mock.patch("cloudinit.net.read_sys_net")
888 @mock.patch("cloudinit.net.get_devicelist")
889 def test_device_driver(self, mock_get_devicelist, mock_read_sys_net,
890 mock_sys_dev_path):
891 devices = {
892 'eth0': {
893 'bridge': False, 'carrier': False, 'dormant': False,
894 'operstate': 'down', 'address': '00:11:22:33:44:55',
895 'device/driver': 'hv_netsvc', 'device/device': '0x3'},
896 'eth1': {
897 'bridge': False, 'carrier': False, 'dormant': False,
898 'operstate': 'down', 'address': '00:11:22:33:44:55',
899 'device/driver': 'mlx4_core', 'device/device': '0x7'},
900 }
901
902 tmp_dir = self.tmp_dir()
903 _setup_test(tmp_dir, mock_get_devicelist,
904 mock_read_sys_net, mock_sys_dev_path,
905 dev_attrs=devices)
906
907 network_cfg = net.generate_fallback_config(config_driver=True)
908 ns = network_state.parse_net_config_data(network_cfg,
909 skip_broken=False)
910
911 render_dir = os.path.join(tmp_dir, "render")
912 os.makedirs(render_dir)
913
914 # don't set rulepath so eni writes them
915 renderer = eni.Renderer(
916 {'eni_path': 'interfaces', 'netrules_path': 'netrules'})
917 renderer.render_network_state(ns, render_dir)
918
919 self.assertTrue(os.path.exists(os.path.join(render_dir,
920 'interfaces')))
921 with open(os.path.join(render_dir, 'interfaces')) as fh:
922 contents = fh.read()
923 print(contents)
924 expected = """
925auto lo
926iface lo inet loopback
927
928auto eth0
929iface eth0 inet dhcp
930"""
931 self.assertEqual(expected.lstrip(), contents.lstrip())
932
933 self.assertTrue(os.path.exists(os.path.join(render_dir, 'netrules')))
934 with open(os.path.join(render_dir, 'netrules')) as fh:
935 contents = fh.read()
936 print(contents)
937 expected_rule = [
938 'SUBSYSTEM=="net"',
939 'ACTION=="add"',
940 'DRIVERS=="hv_netsvc"',
941 'ATTR{address}=="00:11:22:33:44:55"',
942 'NAME="eth0"',
943 ]
944 self.assertEqual(", ".join(expected_rule) + '\n', contents.lstrip())
945
946 @mock.patch("cloudinit.net.sys_dev_path")
947 @mock.patch("cloudinit.net.read_sys_net")
948 @mock.patch("cloudinit.net.get_devicelist")
949 def test_device_driver_blacklist(self, mock_get_devicelist,
950 mock_read_sys_net, mock_sys_dev_path):
951 devices = {
952 'eth1': {
953 'bridge': False, 'carrier': False, 'dormant': False,
954 'operstate': 'down', 'address': '00:11:22:33:44:55',
955 'device/driver': 'hv_netsvc', 'device/device': '0x3'},
956 'eth0': {
957 'bridge': False, 'carrier': False, 'dormant': False,
958 'operstate': 'down', 'address': '00:11:22:33:44:55',
959 'device/driver': 'mlx4_core', 'device/device': '0x7'},
960 }
961
962 tmp_dir = self.tmp_dir()
963 _setup_test(tmp_dir, mock_get_devicelist,
964 mock_read_sys_net, mock_sys_dev_path,
965 dev_attrs=devices)
966
967 blacklist = ['mlx4_core']
968 network_cfg = net.generate_fallback_config(blacklist_drivers=blacklist,
969 config_driver=True)
970 ns = network_state.parse_net_config_data(network_cfg,
971 skip_broken=False)
972
973 render_dir = os.path.join(tmp_dir, "render")
974 os.makedirs(render_dir)
975
976 # don't set rulepath so eni writes them
977 renderer = eni.Renderer(
978 {'eni_path': 'interfaces', 'netrules_path': 'netrules'})
979 renderer.render_network_state(ns, render_dir)
980
981 self.assertTrue(os.path.exists(os.path.join(render_dir,
982 'interfaces')))
983 with open(os.path.join(render_dir, 'interfaces')) as fh:
984 contents = fh.read()
985 print(contents)
986 expected = """
987auto lo
988iface lo inet loopback
989
990auto eth1
991iface eth1 inet dhcp
992"""
993 self.assertEqual(expected.lstrip(), contents.lstrip())
994
995 self.assertTrue(os.path.exists(os.path.join(render_dir, 'netrules')))
996 with open(os.path.join(render_dir, 'netrules')) as fh:
997 contents = fh.read()
998 print(contents)
999 expected_rule = [
1000 'SUBSYSTEM=="net"',
1001 'ACTION=="add"',
1002 'DRIVERS=="hv_netsvc"',
1003 'ATTR{address}=="00:11:22:33:44:55"',
1004 'NAME="eth1"',
1005 ]
1006 self.assertEqual(", ".join(expected_rule) + '\n', contents.lstrip())
1007
1008
871class TestSysConfigRendering(CiTestCase):1009class TestSysConfigRendering(CiTestCase):
8721010
873 @mock.patch("cloudinit.net.sys_dev_path")1011 @mock.patch("cloudinit.net.sys_dev_path")
@@ -1560,6 +1698,118 @@ class TestNetRenderers(CiTestCase):
1560 priority=['sysconfig', 'eni'])1698 priority=['sysconfig', 'eni'])
15611699
15621700
1701class TestGetInterfaces(CiTestCase):
1702 _data = {'bonds': ['bond1'],
1703 'bridges': ['bridge1'],
1704 'vlans': ['bond1.101'],
1705 'own_macs': ['enp0s1', 'enp0s2', 'bridge1-nic', 'bridge1',
1706 'bond1.101', 'lo', 'eth1'],
1707 'macs': {'enp0s1': 'aa:aa:aa:aa:aa:01',
1708 'enp0s2': 'aa:aa:aa:aa:aa:02',
1709 'bond1': 'aa:aa:aa:aa:aa:01',
1710 'bond1.101': 'aa:aa:aa:aa:aa:01',
1711 'bridge1': 'aa:aa:aa:aa:aa:03',
1712 'bridge1-nic': 'aa:aa:aa:aa:aa:03',
1713 'lo': '00:00:00:00:00:00',
1714 'greptap0': '00:00:00:00:00:00',
1715 'eth1': 'aa:aa:aa:aa:aa:01',
1716 'tun0': None},
1717 'drivers': {'enp0s1': 'virtio_net',
1718 'enp0s2': 'e1000',
1719 'bond1': None,
1720 'bond1.101': None,
1721 'bridge1': None,
1722 'bridge1-nic': None,
1723 'lo': None,
1724 'greptap0': None,
1725 'eth1': 'mlx4_core',
1726 'tun0': None}}
1727 data = {}
1728
1729 def _se_get_devicelist(self):
1730 return list(self.data['devices'])
1731
1732 def _se_device_driver(self, name):
1733 return self.data['drivers'][name]
1734
1735 def _se_device_devid(self, name):
1736 return '0x%s' % sorted(list(self.data['drivers'].keys())).index(name)
1737
1738 def _se_get_interface_mac(self, name):
1739 return self.data['macs'][name]
1740
1741 def _se_is_bridge(self, name):
1742 return name in self.data['bridges']
1743
1744 def _se_is_vlan(self, name):
1745 return name in self.data['vlans']
1746
1747 def _se_interface_has_own_mac(self, name):
1748 return name in self.data['own_macs']
1749
1750 def _mock_setup(self):
1751 self.data = copy.deepcopy(self._data)
1752 self.data['devices'] = set(list(self.data['macs'].keys()))
1753 mocks = ('get_devicelist', 'get_interface_mac', 'is_bridge',
1754 'interface_has_own_mac', 'is_vlan', 'device_driver',
1755 'device_devid')
1756 self.mocks = {}
1757 for n in mocks:
1758 m = mock.patch('cloudinit.net.' + n,
1759 side_effect=getattr(self, '_se_' + n))
1760 self.addCleanup(m.stop)
1761 self.mocks[n] = m.start()
1762
1763 def test_gi_includes_duplicate_macs(self):
1764 self._mock_setup()
1765 ret = net.get_interfaces()
1766
1767 self.assertIn('enp0s1', self._se_get_devicelist())
1768 self.assertIn('eth1', self._se_get_devicelist())
1769 found = [ent for ent in ret if 'aa:aa:aa:aa:aa:01' in ent]
1770 self.assertEqual(len(found), 2)
1771
1772 def test_gi_excludes_any_without_mac_address(self):
1773 self._mock_setup()
1774 ret = net.get_interfaces()
1775
1776 self.assertIn('tun0', self._se_get_devicelist())
1777 found = [ent for ent in ret if 'tun0' in ent]
1778 self.assertEqual(len(found), 0)
1779
1780 def test_gi_excludes_stolen_macs(self):
1781 self._mock_setup()
1782 ret = net.get_interfaces()
1783 self.mocks['interface_has_own_mac'].assert_has_calls(
1784 [mock.call('enp0s1'), mock.call('bond1')], any_order=True)
1785 expected = [
1786 ('enp0s2', 'aa:aa:aa:aa:aa:02', 'e1000', '0x5'),
1787 ('enp0s1', 'aa:aa:aa:aa:aa:01', 'virtio_net', '0x4'),
1788 ('eth1', 'aa:aa:aa:aa:aa:01', 'mlx4_core', '0x6'),
1789 ('lo', '00:00:00:00:00:00', None, '0x8'),
1790 ('bridge1-nic', 'aa:aa:aa:aa:aa:03', None, '0x3'),
1791 ]
1792 self.assertEqual(sorted(expected), sorted(ret))
1793
1794 def test_gi_excludes_bridges(self):
1795 self._mock_setup()
1796 # add a device 'b1', make all return they have their "own mac",
1797 # set everything other than 'b1' to be a bridge.
1798 # then expect b1 is the only thing left.
1799 self.data['macs']['b1'] = 'aa:aa:aa:aa:aa:b1'
1800 self.data['drivers']['b1'] = None
1801 self.data['devices'].add('b1')
1802 self.data['bonds'] = []
1803 self.data['own_macs'] = self.data['devices']
1804 self.data['bridges'] = [f for f in self.data['devices'] if f != "b1"]
1805 ret = net.get_interfaces()
1806 self.assertEqual([('b1', 'aa:aa:aa:aa:aa:b1', None, '0x0')], ret)
1807 self.mocks['is_bridge'].assert_has_calls(
1808 [mock.call('bridge1'), mock.call('enp0s1'), mock.call('bond1'),
1809 mock.call('b1')],
1810 any_order=True)
1811
1812
1563class TestGetInterfacesByMac(CiTestCase):1813class TestGetInterfacesByMac(CiTestCase):
1564 _data = {'bonds': ['bond1'],1814 _data = {'bonds': ['bond1'],
1565 'bridges': ['bridge1'],1815 'bridges': ['bridge1'],
@@ -1691,4 +1941,202 @@ def _gzip_data(data):
1691 gzfp.close()1941 gzfp.close()
1692 return iobuf.getvalue()1942 return iobuf.getvalue()
16931943
1944
1945class TestRenameInterfaces(CiTestCase):
1946
1947 @mock.patch('cloudinit.util.subp')
1948 def test_rename_all(self, mock_subp):
1949 renames = [
1950 ('00:11:22:33:44:55', 'interface0', 'virtio_net', '0x3'),
1951 ('00:11:22:33:44:aa', 'interface2', 'virtio_net', '0x5'),
1952 ]
1953 current_info = {
1954 'ens3': {
1955 'downable': True,
1956 'device_id': '0x3',
1957 'driver': 'virtio_net',
1958 'mac': '00:11:22:33:44:55',
1959 'name': 'ens3',
1960 'up': False},
1961 'ens5': {
1962 'downable': True,
1963 'device_id': '0x5',
1964 'driver': 'virtio_net',
1965 'mac': '00:11:22:33:44:aa',
1966 'name': 'ens5',
1967 'up': False},
1968 }
1969 net._rename_interfaces(renames, current_info=current_info)
1970 print(mock_subp.call_args_list)
1971 mock_subp.assert_has_calls([
1972 mock.call(['ip', 'link', 'set', 'ens3', 'name', 'interface0'],
1973 capture=True),
1974 mock.call(['ip', 'link', 'set', 'ens5', 'name', 'interface2'],
1975 capture=True),
1976 ])
1977
1978 @mock.patch('cloudinit.util.subp')
1979 def test_rename_no_driver_no_device_id(self, mock_subp):
1980 renames = [
1981 ('00:11:22:33:44:55', 'interface0', None, None),
1982 ('00:11:22:33:44:aa', 'interface1', None, None),
1983 ]
1984 current_info = {
1985 'eth0': {
1986 'downable': True,
1987 'device_id': None,
1988 'driver': None,
1989 'mac': '00:11:22:33:44:55',
1990 'name': 'eth0',
1991 'up': False},
1992 'eth1': {
1993 'downable': True,
1994 'device_id': None,
1995 'driver': None,
1996 'mac': '00:11:22:33:44:aa',
1997 'name': 'eth1',
1998 'up': False},
1999 }
2000 net._rename_interfaces(renames, current_info=current_info)
2001 print(mock_subp.call_args_list)
2002 mock_subp.assert_has_calls([
2003 mock.call(['ip', 'link', 'set', 'eth0', 'name', 'interface0'],
2004 capture=True),
2005 mock.call(['ip', 'link', 'set', 'eth1', 'name', 'interface1'],
2006 capture=True),
2007 ])
2008
2009 @mock.patch('cloudinit.util.subp')
2010 def test_rename_all_bounce(self, mock_subp):
2011 renames = [
2012 ('00:11:22:33:44:55', 'interface0', 'virtio_net', '0x3'),
2013 ('00:11:22:33:44:aa', 'interface2', 'virtio_net', '0x5'),
2014 ]
2015 current_info = {
2016 'ens3': {
2017 'downable': True,
2018 'device_id': '0x3',
2019 'driver': 'virtio_net',
2020 'mac': '00:11:22:33:44:55',
2021 'name': 'ens3',
2022 'up': True},
2023 'ens5': {
2024 'downable': True,
2025 'device_id': '0x5',
2026 'driver': 'virtio_net',
2027 'mac': '00:11:22:33:44:aa',
2028 'name': 'ens5',
2029 'up': True},
2030 }
2031 net._rename_interfaces(renames, current_info=current_info)
2032 print(mock_subp.call_args_list)
2033 mock_subp.assert_has_calls([
2034 mock.call(['ip', 'link', 'set', 'ens3', 'down'], capture=True),
2035 mock.call(['ip', 'link', 'set', 'ens3', 'name', 'interface0'],
2036 capture=True),
2037 mock.call(['ip', 'link', 'set', 'ens5', 'down'], capture=True),
2038 mock.call(['ip', 'link', 'set', 'ens5', 'name', 'interface2'],
2039 capture=True),
2040 mock.call(['ip', 'link', 'set', 'interface0', 'up'], capture=True),
2041 mock.call(['ip', 'link', 'set', 'interface2', 'up'], capture=True)
2042 ])
2043
2044 @mock.patch('cloudinit.util.subp')
2045 def test_rename_duplicate_macs(self, mock_subp):
2046 renames = [
2047 ('00:11:22:33:44:55', 'eth0', 'hv_netsvc', '0x3'),
2048 ('00:11:22:33:44:55', 'vf1', 'mlx4_core', '0x5'),
2049 ]
2050 current_info = {
2051 'eth0': {
2052 'downable': True,
2053 'device_id': '0x3',
2054 'driver': 'hv_netsvc',
2055 'mac': '00:11:22:33:44:55',
2056 'name': 'eth0',
2057 'up': False},
2058 'eth1': {
2059 'downable': True,
2060 'device_id': '0x5',
2061 'driver': 'mlx4_core',
2062 'mac': '00:11:22:33:44:55',
2063 'name': 'eth1',
2064 'up': False},
2065 }
2066 net._rename_interfaces(renames, current_info=current_info)
2067 print(mock_subp.call_args_list)
2068 mock_subp.assert_has_calls([
2069 mock.call(['ip', 'link', 'set', 'eth1', 'name', 'vf1'],
2070 capture=True),
2071 ])
2072
2073 @mock.patch('cloudinit.util.subp')
2074 def test_rename_duplicate_macs_driver_no_devid(self, mock_subp):
2075 renames = [
2076 ('00:11:22:33:44:55', 'eth0', 'hv_netsvc', None),
2077 ('00:11:22:33:44:55', 'vf1', 'mlx4_core', None),
2078 ]
2079 current_info = {
2080 'eth0': {
2081 'downable': True,
2082 'device_id': '0x3',
2083 'driver': 'hv_netsvc',
2084 'mac': '00:11:22:33:44:55',
2085 'name': 'eth0',
2086 'up': False},
2087 'eth1': {
2088 'downable': True,
2089 'device_id': '0x5',
2090 'driver': 'mlx4_core',
2091 'mac': '00:11:22:33:44:55',
2092 'name': 'eth1',
2093 'up': False},
2094 }
2095 net._rename_interfaces(renames, current_info=current_info)
2096 print(mock_subp.call_args_list)
2097 mock_subp.assert_has_calls([
2098 mock.call(['ip', 'link', 'set', 'eth1', 'name', 'vf1'],
2099 capture=True),
2100 ])
2101
2102 @mock.patch('cloudinit.util.subp')
2103 def test_rename_multi_mac_dups(self, mock_subp):
2104 renames = [
2105 ('00:11:22:33:44:55', 'eth0', 'hv_netsvc', '0x3'),
2106 ('00:11:22:33:44:55', 'vf1', 'mlx4_core', '0x5'),
2107 ('00:11:22:33:44:55', 'vf2', 'mlx4_core', '0x7'),
2108 ]
2109 current_info = {
2110 'eth0': {
2111 'downable': True,
2112 'device_id': '0x3',
2113 'driver': 'hv_netsvc',
2114 'mac': '00:11:22:33:44:55',
2115 'name': 'eth0',
2116 'up': False},
2117 'eth1': {
2118 'downable': True,
2119 'device_id': '0x5',
2120 'driver': 'mlx4_core',
2121 'mac': '00:11:22:33:44:55',
2122 'name': 'eth1',
2123 'up': False},
2124 'eth2': {
2125 'downable': True,
2126 'device_id': '0x7',
2127 'driver': 'mlx4_core',
2128 'mac': '00:11:22:33:44:55',
2129 'name': 'eth2',
2130 'up': False},
2131 }
2132 net._rename_interfaces(renames, current_info=current_info)
2133 print(mock_subp.call_args_list)
2134 mock_subp.assert_has_calls([
2135 mock.call(['ip', 'link', 'set', 'eth1', 'name', 'vf1'],
2136 capture=True),
2137 mock.call(['ip', 'link', 'set', 'eth2', 'name', 'vf2'],
2138 capture=True),
2139 ])
2140
2141
1694# vi: ts=4 expandtab2142# vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches