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
1diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
2index 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
294diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
295index 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',
307diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py
308index 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
323diff --git a/cloudinit/net/udev.py b/cloudinit/net/udev.py
324index 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 ])
351diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
352index 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
458diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
459index 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':
576diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/test_datasource/test_common.py
577index 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,
588diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
589index 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

Subscribers

People subscribed via source and target branches