Merge lp:~jfontan/cloud-init/opennebula-4.8 into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Javi Fontan
Status: Rejected
Rejected by: Scott Moser
Proposed branch: lp:~jfontan/cloud-init/opennebula-4.8
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 522 lines (+270/-90)
4 files modified
cloudinit/distros/__init__.py (+19/-6)
cloudinit/distros/rhel.py (+12/-0)
cloudinit/sources/DataSourceOpenNebula.py (+137/-52)
tests/unittests/test_datasource/test_opennebula.py (+102/-32)
To merge this branch: bzr merge lp:~jfontan/cloud-init/opennebula-4.8
Reviewer Review Type Date Requested Status
cloud-init Commiters Pending
Review via email: mp+234207@code.launchpad.net

Description of the change

Changes to bring new functionality to the OpenNebula DataSource (ONE 4.8) and fix problems with the new interface naming scheme. It also addresses some problems with RHEL/CentOS 7 that made the old version incompatible.

There is a change that I'm not sure you are happy with it. I've added a ifdown command before ifup as in Ubuntu the interface did not get configured as the interface was already up. If there is a better way to fix this I will be happy to change the code.

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

I'm sorry for horribly delinquent behavior.

Javi, thanks for your submission.
If you're still around
a.) is this still relevant / needed?
b.) Can you sign the contributors agreement? http://www.ubuntu.com/legal/contributors please feel free to ping me in irc or email if you have questions.

Revision history for this message
Javi Fontan (jfontan) wrote :

Sorry for the late reply.

The changes are still needed. I'm not sure if they can be merged as is
as I have not been following cloud-init development recently. As
stated I'm not sure you will like the ifdown/ifup of interfaces.

I've already signed the agreement for a previous contribution to cloud-init.

I'm jfontan in freenode if you need anything.

Thanks

On Wed, Apr 6, 2016 at 9:38 PM, Scott Moser <email address hidden> wrote:
>
> I'm sorry for horribly delinquent behavior.
>
> Javi, thanks for your submission.
> If you're still around
> a.) is this still relevant / needed?
> b.) Can you sign the contributors agreement? http://www.ubuntu.com/legal/contributors please feel free to ping me in irc or email if you have questions.
>
> --
> https://code.launchpad.net/~jfontan/cloud-init/opennebula-4.8/+merge/234207
> You are the owner of lp:~jfontan/cloud-init/opennebula-4.8.

--
Javier Fontán Muiños
Developer
OpenNebula - Flexible Enterprise Cloud Made Simple
www.OpenNebula.org | @OpenNebula | github.com/jfontan

Revision history for this message
Scott Moser (smoser) wrote :

Hello,
Thank you for taking the time to contribute to cloud-init. Cloud-init has moved its revision control system to git. As a result, we are marking all bzr merge proposals as 'rejected'. If you would like to re-submit this proposal for review, please do so by following the current HACKING documentation at http://cloudinit.readthedocs.io/en/latest/topics/hacking.html .

Since network configuration has changed dramatically, I"m not sure if this change is still relevant.

Unmerged revisions

1012. By Javi Fontan

Update OpenNebula DataSource to the latest version

Now network interfaces are matched using MAC addresses instead of name,
this makes it work with the new interface naming scheme.

Also added support for IPv6, select interface for gateway, search domain
and fixes for RHEL/CentOS 7

1011. By Javi Fontan

Add distro functions for OpenNebula DataSource

OpenNebula datasource need to disable NetworkManager and use runuser instead
of sudo in RHEL/CentOS

1010. By Javi Fontan

Bring down interface before configuring it

If the interface is already up the ifup command will have no effect in
some distributions.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/distros/__init__.py'
2--- cloudinit/distros/__init__.py 2014-09-10 18:32:37 +0000
3+++ cloudinit/distros/__init__.py 2014-09-10 21:01:03 +0000
4@@ -271,13 +271,19 @@
5 util.write_file(self.hosts_fn, contents.getvalue(), mode=0644)
6
7 def _bring_up_interface(self, device_name):
8- cmd = ['ifup', device_name]
9- LOG.debug("Attempting to run bring up interface %s using command %s",
10- device_name, cmd)
11+ down_cmd = ['ifdown', device_name]
12+ up_cmd = ['ifup', device_name]
13+ LOG.debug("Attempting to run bring up interface %s using commands %s, %s",
14+ device_name, down_cmd, up_cmd)
15 try:
16- (_out, err) = util.subp(cmd)
17- if len(err):
18- LOG.warn("Running %s resulted in stderr output: %s", cmd, err)
19+ (_out, err) = util.subp(down_cmd)
20+ if len(err):
21+ LOG.warn("Running %s resulted in stderr output: %s", down_cmd, err)
22+
23+ (_out, err) = util.subp(up_cmd)
24+ if len(err):
25+ LOG.warn("Running %s resulted in stderr output: %s", up_cmd, err)
26+
27 return True
28 except util.ProcessExecutionError:
29 util.logexc(LOG, "Running interface command %s failed", cmd)
30@@ -525,6 +531,13 @@
31 util.subp(['usermod', '-a', '-G', name, member])
32 LOG.info("Added user '%s' to group '%s'" % (member, name))
33
34+ # Command used to execute command with an specific user
35+ def switch_user_cmd(self, user):
36+ return ['sudo', '-u', user]
37+
38+ def disable_network_manager(self):
39+ pass
40+
41
42 def _get_package_mirror_info(mirror_info, availability_zone=None,
43 mirror_filter=util.search_for_mirror):
44
45=== modified file 'cloudinit/distros/rhel.py'
46--- cloudinit/distros/rhel.py 2014-02-03 22:03:14 +0000
47+++ cloudinit/distros/rhel.py 2014-09-10 21:01:03 +0000
48@@ -208,3 +208,15 @@
49 def update_package_sources(self):
50 self._runner.run("update-sources", self.package_command,
51 ["makecache"], freq=PER_INSTANCE)
52+
53+ def switch_user_cmd(self, user):
54+ # In RHEL/CentOS sudo has requiretty defined and sudo can not be used
55+ return ['runuser', '-u', user, '--']
56+
57+ def disable_network_manager(self):
58+ # ifup fails if NetworkManager is running
59+ try:
60+ (out, _err) = util.subp(["systemctl", "stop", "NetworkManager"])
61+ (out, _err) = util.subp(["systemctl", "disable", "NetworkManager"])
62+ except util.ProcessExecutionError:
63+ util.logexc(LOG, "Disable NetworkManager command failed")
64\ No newline at end of file
65
66=== modified file 'cloudinit/sources/DataSourceOpenNebula.py'
67--- cloudinit/sources/DataSourceOpenNebula.py 2014-08-26 19:53:41 +0000
68+++ cloudinit/sources/DataSourceOpenNebula.py 2014-09-10 21:01:03 +0000
69@@ -42,7 +42,6 @@
70 CONTEXT_DISK_FILES = ["context.sh"]
71 VALID_DSMODES = ("local", "net", "disabled")
72
73-
74 class DataSourceOpenNebula(sources.DataSource):
75 def __init__(self, sys_cfg, distro, paths):
76 sources.DataSource.__init__(self, sys_cfg, distro, paths)
77@@ -69,10 +68,12 @@
78 for cdev in candidates:
79 try:
80 if os.path.isdir(self.seed_dir):
81- results = read_context_disk_dir(cdev, asuser=parseuser)
82+ results = read_context_disk_dir(cdev, { 'distro': self.distro,
83+ 'asuser': parseuser })
84 elif cdev.startswith("/dev"):
85 results = util.mount_cb(cdev, read_context_disk_dir,
86- data=parseuser)
87+ data={ 'distro': self.distro,
88+ 'asuser': parseuser })
89 except NonContextDiskDir:
90 continue
91 except BrokenContextDiskDir as exc:
92@@ -149,63 +150,99 @@
93
94 class OpenNebulaNetwork(object):
95 REG_DEV_MAC = re.compile(
96- r'^\d+: (eth\d+):.*?link\/ether (..:..:..:..:..:..) ?',
97+ r'^\d+: (\w+):.*?link\/\w+ (..:..:..:..:..:..) ?',
98 re.MULTILINE | re.DOTALL)
99
100- def __init__(self, ip, context):
101+ def __init__(self, ip, context, distro):
102 self.ip = ip
103 self.context = context
104 self.ifaces = self.get_ifaces()
105+ self.distro = distro
106
107 def get_ifaces(self):
108- return self.REG_DEV_MAC.findall(self.ip)
109+ list = self.REG_DEV_MAC.findall(self.ip)
110+ ifaces = dict()
111+ for l in list:
112+ ifaces[l[1]] = l[0]
113+ return ifaces
114
115 def mac2ip(self, mac):
116 components = mac.split(':')[2:]
117 return [str(int(c, 16)) for c in components]
118
119+ def get_iface_var(self, dev, var):
120+ var_name = dev.upper() + '_' + var
121+ if var_name in self.context:
122+ return self.context[var_name]
123+ else:
124+ return None
125+
126 def get_ip(self, dev, components):
127- var_name = dev.upper() + '_IP'
128- if var_name in self.context:
129- return self.context[var_name]
130+ var = self.get_iface_var(dev, 'IP')
131+ if var:
132+ return var
133 else:
134 return '.'.join(components)
135
136 def get_mask(self, dev):
137- var_name = dev.upper() + '_MASK'
138- if var_name in self.context:
139- return self.context[var_name]
140+ var = self.get_iface_var(dev, 'MASK')
141+ if var:
142+ return var
143 else:
144 return '255.255.255.0'
145
146 def get_network(self, dev, components):
147- var_name = dev.upper() + '_NETWORK'
148- if var_name in self.context:
149- return self.context[var_name]
150+ var = self.get_iface_var(dev, 'NETWORK')
151+ if var:
152+ return var
153 else:
154 return '.'.join(components[:-1]) + '.0'
155
156+ def is_gateway_iface(self, dev):
157+ if 'GATEWAY_IFACE' in self.context:
158+ val=self.context['GATEWAY_IFACE']
159+ dev_num=val.replace('ETH', '')
160+ return ('ETH'+dev_num) == dev
161+ else:
162+ return True
163+
164 def get_gateway(self, dev):
165- var_name = dev.upper() + '_GATEWAY'
166- if var_name in self.context:
167- return self.context[var_name]
168+ if self.is_gateway_iface(dev):
169+ return self.get_iface_var(dev, 'GATEWAY')
170 else:
171 return None
172
173 def get_dns(self, dev):
174- var_name = dev.upper() + '_DNS'
175- if var_name in self.context:
176- return self.context[var_name]
177- else:
178- return None
179+ return self.get_iface_var(dev, 'DNS')
180
181 def get_domain(self, dev):
182- var_name = dev.upper() + '_DOMAIN'
183- if var_name in self.context:
184- return self.context[var_name]
185+ var = self.get_iface_var(dev, 'SEARCH_DOMAIN')
186+ if var:
187+ return var
188+
189+ return self.get_iface_var(dev, 'DOMAIN')
190+
191+ def get_force_ipv4(self, dev):
192+ return self.get_iface_var(dev, 'CONTEXT_FORCE_IPV4')
193+
194+ def get_ip6(self, dev):
195+ return self.get_iface_var(dev, 'IP6')
196+
197+ def get_gateway6(self, dev):
198+ if self.is_gateway_iface(dev):
199+ return self.get_iface_var(dev, 'GATEWAY6')
200 else:
201 return None
202
203+ def get_context_interfaces(self):
204+
205+ def device_mac(t): return re.match(r"ETH\d+_MAC", t)
206+
207+ mac_vars = filter(device_mac, self.context.keys())
208+
209+ context_interfaces = [v.split('_')[0] for v in mac_vars]
210+ return context_interfaces
211+
212 def gen_conf(self):
213 global_dns = []
214 if 'DNS' in self.context:
215@@ -216,34 +253,65 @@
216 conf.append('iface lo inet loopback')
217 conf.append('')
218
219- for i in self.ifaces:
220- dev = i[0]
221- mac = i[1]
222+ context_interfaces = self.get_context_interfaces()
223+
224+ if len(context_interfaces) and self.distro:
225+ self.distro.disable_network_manager()
226+
227+ for interface in context_interfaces:
228+ mac = self.context[interface+"_MAC"]
229+ dev = self.ifaces[mac]
230+
231 ip_components = self.mac2ip(mac)
232
233- conf.append('auto ' + dev)
234- conf.append('iface ' + dev + ' inet static')
235- conf.append(' address ' + self.get_ip(dev, ip_components))
236- conf.append(' network ' + self.get_network(dev, ip_components))
237- conf.append(' netmask ' + self.get_mask(dev))
238-
239- gateway = self.get_gateway(dev)
240- if gateway:
241- conf.append(' gateway ' + gateway)
242-
243- domain = self.get_domain(dev)
244- if domain:
245- conf.append(' dns-search ' + domain)
246-
247- # add global DNS servers to all interfaces
248- dns = self.get_dns(dev)
249+ all_dns = None
250+ dns = self.get_dns(interface)
251 if global_dns or dns:
252 all_dns = global_dns
253 if dns:
254 all_dns.append(dns)
255- conf.append(' dns-nameservers ' + ' '.join(all_dns))
256-
257- conf.append('')
258+
259+ ip6 = self.get_ip6(dev)
260+ conf.append('auto ' + dev)
261+
262+ if self.get_force_ipv4(dev) or not ip6:
263+ conf.append('iface ' + dev + ' inet static')
264+ conf.append(' address ' + self.get_ip(interface, ip_components))
265+ conf.append(' network ' + self.get_network(interface, ip_components))
266+ conf.append(' netmask ' + self.get_mask(interface))
267+
268+ gateway = self.get_gateway(interface)
269+ if gateway:
270+ conf.append(' gateway ' + gateway)
271+
272+ domain = self.get_domain(interface)
273+ if domain:
274+ conf.append(' dns-search ' + domain)
275+
276+ # add global DNS servers to all interfaces
277+ if all_dns:
278+ conf.append(' dns-nameservers ' + ' '.join(all_dns))
279+
280+ conf.append('')
281+
282+ if ip6:
283+ conf.append('iface ' + dev + ' inet6 static')
284+ conf.append(' address ' + ip6)
285+ conf.append(' netmask 64')
286+
287+ gateway = self.get_gateway6(dev)
288+ if gateway:
289+ conf.append(' gateway ' + gateway)
290+
291+ domain = self.get_domain(interface)
292+ if domain:
293+ conf.append(' dns-search ' + domain)
294+
295+ # add global DNS servers to all interfaces
296+ if all_dns:
297+ conf.append(' dns-nameservers ' + ' '.join(all_dns))
298+
299+ conf.append('')
300
301 return "\n".join(conf)
302
303@@ -353,12 +421,21 @@
304 return ret
305
306
307-def read_context_disk_dir(source_dir, asuser=None):
308+def read_context_disk_dir(source_dir, options={}):
309 """
310 read_context_disk_dir(source_dir):
311 read source_dir and return a tuple with metadata dict and user-data
312 string populated. If not a valid dir, raise a NonContextDiskDir
313 """
314+
315+ asuser = None
316+ if options.has_key('asuser'):
317+ asuser = options['asuser']
318+
319+ distro = None
320+ if options.has_key('distro'):
321+ distro = options['distro']
322+
323 found = {}
324 for af in CONTEXT_DISK_FILES:
325 fn = os.path.join(source_dir, af)
326@@ -382,7 +459,14 @@
327 with open(os.path.join(source_dir, 'context.sh'), 'r') as f:
328 content = f.read().strip()
329
330- context = parse_shell_config(content, asuser=asuser)
331+ if distro:
332+ switch_cmd = distro.switch_user_cmd
333+ else:
334+ switch_cmd = None
335+
336+ context = parse_shell_config(content,
337+ asuser=asuser,
338+ switch_user_cb=switch_cmd)
339 except util.ProcessExecutionError as e:
340 raise BrokenContextDiskDir("Error processing context.sh: %s" % (e))
341 except IOError as e:
342@@ -409,7 +493,7 @@
343
344 # custom hostname -- try hostname or leave cloud-init
345 # itself create hostname from IP address later
346- for k in ('HOSTNAME', 'PUBLIC_IP', 'IP_PUBLIC', 'ETH0_IP'):
347+ for k in ('SET_HOSTNAME', 'HOSTNAME', 'PUBLIC_IP', 'IP_PUBLIC', 'ETH0_IP'):
348 if k in context:
349 results['metadata']['local-hostname'] = context[k]
350 break
351@@ -436,7 +520,7 @@
352 for k in context.keys():
353 if re.match(r'^ETH\d+_IP$', k):
354 (out, _) = util.subp(['/sbin/ip', 'link'])
355- net = OpenNebulaNetwork(out, context)
356+ net = OpenNebulaNetwork(out, context, distro)
357 results['network-interfaces'] = net.gen_conf()
358 break
359
360@@ -453,3 +537,4 @@
361 # Return a list of data sources that match this set of dependencies
362 def get_datasource_list(depends):
363 return sources.list_from_depends(depends, datasources)
364+
365
366=== modified file 'tests/unittests/test_datasource/test_opennebula.py'
367--- tests/unittests/test_datasource/test_opennebula.py 2014-07-23 16:50:45 +0000
368+++ tests/unittests/test_datasource/test_opennebula.py 2014-09-10 21:01:03 +0000
369@@ -234,29 +234,17 @@
370 super(TestOpenNebulaNetwork, self).setUp()
371
372 def test_lo(self):
373- net = ds.OpenNebulaNetwork('', {})
374- self.assertEqual(net.gen_conf(), u'''\
375-auto lo
376-iface lo inet loopback
377-''')
378-
379- def test_eth0(self):
380- net = ds.OpenNebulaNetwork(CMD_IP_OUT, {})
381- self.assertEqual(net.gen_conf(), u'''\
382-auto lo
383-iface lo inet loopback
384-
385-auto eth0
386-iface eth0 inet static
387- address 10.18.1.1
388- network 10.18.1.0
389- netmask 255.255.255.0
390+ net = ds.OpenNebulaNetwork('', {}, None)
391+ self.assertEqual(net.gen_conf(), u'''\
392+auto lo
393+iface lo inet loopback
394 ''')
395
396 def test_eth0_override(self):
397 context = {
398 'DNS': '1.2.3.8',
399 'ETH0_IP': '1.2.3.4',
400+ 'ETH0_MAC': '02:00:0a:12:01:01',
401 'ETH0_NETWORK': '1.2.3.0',
402 'ETH0_MASK': '255.255.0.0',
403 'ETH0_GATEWAY': '1.2.3.5',
404@@ -264,21 +252,103 @@
405 'ETH0_DNS': '1.2.3.6 1.2.3.7'
406 }
407
408- net = ds.OpenNebulaNetwork(CMD_IP_OUT, context)
409- self.assertEqual(net.gen_conf(), u'''\
410-auto lo
411-iface lo inet loopback
412-
413-auto eth0
414-iface eth0 inet static
415- address 1.2.3.4
416- network 1.2.3.0
417- netmask 255.255.0.0
418- gateway 1.2.3.5
419- dns-search example.com
420- dns-nameservers 1.2.3.8 1.2.3.6 1.2.3.7
421-''')
422-
423+ net = ds.OpenNebulaNetwork(CMD_IP_OUT, context, None)
424+ self.assertEqual(net.gen_conf(), u'''\
425+auto lo
426+iface lo inet loopback
427+
428+auto eth0
429+iface eth0 inet static
430+ address 1.2.3.4
431+ network 1.2.3.0
432+ netmask 255.255.0.0
433+ gateway 1.2.3.5
434+ dns-search example.com
435+ dns-nameservers 1.2.3.8 1.2.3.6 1.2.3.7
436+''')
437+
438+ def test_eth0_override_with_gateway(self):
439+ context = {
440+ 'DNS': '1.2.3.8',
441+ 'ETH0_IP': '1.2.3.4',
442+ 'ETH0_MAC': '02:00:0a:12:01:01',
443+ 'ETH0_NETWORK': '1.2.3.0',
444+ 'ETH0_MASK': '255.255.0.0',
445+ 'ETH0_GATEWAY': '1.2.3.5',
446+ 'ETH0_SEARCH_DOMAIN': 'example.com',
447+ 'ETH0_DNS': '1.2.3.6 1.2.3.7',
448+ 'GATEWAY_IFACE': 'ETH0'
449+ }
450+
451+ net = ds.OpenNebulaNetwork(CMD_IP_OUT, context, None)
452+ self.assertEqual(net.gen_conf(), u'''\
453+auto lo
454+iface lo inet loopback
455+
456+auto eth0
457+iface eth0 inet static
458+ address 1.2.3.4
459+ network 1.2.3.0
460+ netmask 255.255.0.0
461+ gateway 1.2.3.5
462+ dns-search example.com
463+ dns-nameservers 1.2.3.8 1.2.3.6 1.2.3.7
464+''')
465+
466+ def test_eth0_override_without_gateway(self):
467+ context = {
468+ 'DNS': '1.2.3.8',
469+ 'ETH0_IP': '1.2.3.4',
470+ 'ETH0_MAC': '02:00:0a:12:01:01',
471+ 'ETH0_NETWORK': '1.2.3.0',
472+ 'ETH0_MASK': '255.255.0.0',
473+ 'ETH0_GATEWAY': '1.2.3.5',
474+ 'ETH0_DOMAIN': 'example.com',
475+ 'ETH0_DNS': '1.2.3.6 1.2.3.7',
476+ 'GATEWAY_IFACE': '1'
477+ }
478+
479+ net = ds.OpenNebulaNetwork(CMD_IP_OUT, context, None)
480+ self.assertEqual(net.gen_conf(), u'''\
481+auto lo
482+iface lo inet loopback
483+
484+auto eth0
485+iface eth0 inet static
486+ address 1.2.3.4
487+ network 1.2.3.0
488+ netmask 255.255.0.0
489+ dns-search example.com
490+ dns-nameservers 1.2.3.8 1.2.3.6 1.2.3.7
491+''')
492+
493+ def test_eth0_override_with_ipv6(self):
494+ context = {
495+ 'DNS': '1.2.3.8',
496+ 'ETH0_IP': '1.2.3.4',
497+ 'ETH0_MAC': '02:00:0a:12:01:01',
498+ 'ETH0_NETWORK': '1.2.3.0',
499+ 'ETH0_MASK': '255.255.0.0',
500+ 'ETH0_GATEWAY': '1.2.3.5',
501+ 'ETH0_DOMAIN': 'example.com',
502+ 'ETH0_DNS': '1.2.3.6 1.2.3.7',
503+ 'ETH0_IP6': '2001:470:801f::1',
504+ 'ETH0_GATEWAY6': '2001:470:1f05:15a::1'
505+ }
506+
507+ net = ds.OpenNebulaNetwork(CMD_IP_OUT, context, None)
508+ self.assertEqual(net.gen_conf(), u'''\
509+auto lo
510+iface lo inet loopback
511+
512+auto eth0
513+iface eth0 inet6 static
514+ address 2001:470:801f::1
515+ netmask 64
516+ gateway 2001:470:1f05:15a::1
517+ dns-search example.com
518+ dns-nameservers 1.2.3.8 1.2.3.6 1.2.3.7
519+''')
520
521 class TestParseShellConfig(MockerTestCase):
522 def test_no_seconds(self):