Merge lp:~vlastimil-holer/cloud-init/opennebula into lp:~cloud-init-dev/cloud-init/trunk
- opennebula
- Merge into trunk
Status: | Superseded |
---|---|
Proposed branch: | lp:~vlastimil-holer/cloud-init/opennebula |
Merge into: | lp:~cloud-init-dev/cloud-init/trunk |
Diff against target: |
759 lines (+691/-2) 7 files modified
cloudinit/settings.py (+1/-0) cloudinit/sources/DataSourceOpenNebula.py (+367/-0) cloudinit/sources/__init__.py (+9/-2) cloudinit/util.py (+7/-0) doc/rtd/topics/datasources.rst (+6/-0) doc/sources/opennebula/README.rst (+132/-0) tests/unittests/test_datasource/test_opennebula.py (+169/-0) |
To merge this branch: | bzr merge lp:~vlastimil-holer/cloud-init/opennebula |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Scott Moser | Needs Fixing | ||
Joshua Harlow | Pending | ||
Review via email: mp+149953@code.launchpad.net |
This proposal supersedes a proposal from 2012-10-03.
This proposal has been superseded by a proposal from 2013-09-06.
Commit message
Description of the change
I'm resubmitting review proposal for OpenNebula support in cloud-init. I have fixed all comments including unit tests. Javi Fontan from OpenNebula.org also added support for static network configuration. All configuration options are now well documented in README.rst. I'm already using previous and current patches on CentOS 6.3 and Debian 6 images, so there is also real environment testing.
---
These patches are modified existing OpenStacks' ConfigDrive datasource to suite requirements of the OpenNebula (http://
In OpenNebula context variables aren't formalized as in OpenStack, people mostly write their own contextualization scripts and recommends image users what context variables should be exported. I have taken variables seen in most examples (SSH_KEY, HOSTNAME, PUBLIC_IP) to control cloud-init's behaviour.
Joshua Harlow (harlowja) wrote : Posted in a previous version of this proposal | # |
Joshua Harlow (harlowja) wrote : Posted in a previous version of this proposal | # |
Also some tests would be super-great :-)
Vlastimil Holer (vlastimil-holer) wrote : Posted in a previous version of this proposal | # |
> Just a few comments, otherwise seems cool.
>
> Is 'NonConfigDriveDir' (exception afaik) supposed to be brought in somewhere?
Fixed, leftover from ConfigDrive class.
> Should this ds class be derived from the config drive datasource?
> - This is due to the new overload that was just added there
> for mapping device names to actual devices. I noticed this
> code does not have that function either (without it fstab
> will not be adjusted). So perhaps its better to inherit
> if the functionality is the same?? Perhaps override getdata()
> but still use the rest of the parent?
If I understand right, it's not any problem here. OpenNebula's metadata
aren't strictly specified as in OpenStack or EC2. They all are just user
specified and metadata I currently depend on were just mentioned in
their documentation or example scripts, I haven't seen anything like
block device mappings so far. Since there are no default metadata for
device mapping, I think it's better not to invent this functionality now.
You can check here:
http://
All metadata are just optional and user specified, it's common
to have there SSH public key or static network configuration.
> For capturing 'except subprocess.
> catch the one in 'util.py' instead (its not the same exception - although
> maybe it should be).
Fixed.
> Is it possible add more comments as to what the context.sh parsing is doing?
> Ie: ''comm -23 <(set | sort -u) <(echo "$VARS") | egrep -v
> "^(VARS|
Explanation added into code. I know this construction isn't very nice,
but IMHO it's easiest way how to read context variables. Bash dumps
them just as easy parsable (single line) way.
> context_
> 205 + replace(
> 206 + replace(
> 207 + replace(
> 208 + replace("\\'","'")
>
> Should this just be re.group(
> >>> print c
> \n\tblahblah\n\'
> >>> print c.decode(
>
> blahblah
> '
Yes, thanks, fixed.
> Also some tests would be super-great :-)
On my TODO list, will do that soon.
Joshua Harlow (harlowja) wrote : | # |
Ok, one last comment.
Is the modification of "cloudinit/
- 679. By Vlastimil Holer
-
Remove commit "Add resolv.conf configuration function"
Vlastimil Holer (vlastimil-holer) wrote : | # |
> Ok, one last comment.
>
> Is the modification of "cloudinit/
> just leftover code that got picked up in the review?
You are right, this was leftover after Javi's first version of static network configuration.
Currently we are already generating full Debian style network configuration including
"dns-search|
converted and applied into /etc/resolv.conf inside your code already.
Sorry fot that. Change has been uncommited.
Scott Moser (smoser) wrote : | # |
Some thoughts:
* run ./tools/run-pep8 tests/unittests
and ./tools/run-pylint tests/unittests
You'll see lots of (sorry) nit-picky things to fix.
* how does 'user_dsmode' get set? I guess through 'context.sh' ?
* I guess I might like to see the reading of context.sh in its own method for
easier testing.
* populate_dir (copied to your test) is now in unittests/
The one thing I really do not like here is that context.sh is explicitly
executed (by bash), as opposed to being parsed. The issue is that I can
essentially do *anything* here. I could attach a disk labeled CDROM, with
context.sh and execute arbitrary code as root.
This isn't terribly a security issue, since executing code as root is generally
cloud-init's goal, but its very inconsistent with other datasources.
Also, fwiw, I've been wanting to explicitly add a "execute something as root
really early" hook. The goal would be to allow you to tweak an image as early
as possible, even fiddling with or fixing cloud-init.
So, please fix the pep8 and pylint, and, are people actually expecting code to be executed? Ie, do they think they can build/provide a context.sh that executes code?
- 680. By Vlastimil Holer
-
Apply pep8.patch by Javier Fontan <email address hidden>
- 681. By Vlastimil Holer
-
PEP8 fixes.
Javi Fontan (jfontan) wrote : | # |
"context.sh" is generated by OpenNebula and is a shell script with shell variables. It is meant to be executed by some init scripts in the VM to get that information. It should not contain any "executable" code, at least OpenNebula does not add code to it.
The problem with parsing that file is that some variables can be multiline and and parsing is not that straightforward. For example:
# Context variables generated by OpenNebula
DISK_ID='1'
ETH0_DNS='10.0.0.1'
ETH0_GATEWAY=
ETH0_IP='10.0.0.72'
ETH0_MASK=
FILES_DS=
FILES_URL='http://
INIT_SCRIPTS=
MANIFEST=
SSH_PUBLIC_
ssh-dss AAAAB3NzaC1kc3M
I don't think creating a parser for these files is worth the job.
- 682. By Vlastimil Holer
-
Apply parse.diff by Javier Fontan <email address hidden>
- 683. By Vlastimil Holer
-
Fix RE matching context variables. Test cleanups.
- 684. By Vlastimil Holer
-
Search for contextualization CDROM by LABEL=CONTEXT
- 685. By Vlastimil Holer
-
Merged trunk lp:cloud-init
- 686. By Vlastimil Holer
-
PEP8 and Pylint fixes. Move context.sh "parser" into separate
function. Fix fetching user specified dsmode (from context).
Rename context_sh->context. Reuse unittests. helpers. populate_ dir. - 687. By Vlastimil Holer
-
Replace RE context.sh parser with Scott's rewrite of bash dumper. Upper case context variable names.
- 688. By Vlastimil Holer
-
Fix pylint complain on toks.split('.')
- 689. By Vlastimil Holer
-
Configurable OpenNebula:
:parseuser. Seed search dir+dev merge.
Eat shell parser error output. Few tests for tests for get_data. - 690. By Vlastimil Holer
-
Update OpenNebula documentation (parseuser, more fs. labels, K-V single quotes)
- 691. By Vlastimil Holer
-
Detect invalid system user for test
- 692. By Vlastimil Holer
-
Fix detection of ETHx_IP context variable, add test.
- 693. By Vlastimil Holer
- 694. By Vlastimil Holer
-
All fake util.find_devs_with set before try-finally section
Unmerged revisions
Preview Diff
1 | === modified file 'cloudinit/settings.py' |
2 | --- cloudinit/settings.py 2013-07-18 21:37:18 +0000 |
3 | +++ cloudinit/settings.py 2013-09-05 16:03:43 +0000 |
4 | @@ -31,6 +31,7 @@ |
5 | 'datasource_list': [ |
6 | 'NoCloud', |
7 | 'ConfigDrive', |
8 | + 'OpenNebula', |
9 | 'Azure', |
10 | 'AltCloud', |
11 | 'OVF', |
12 | |
13 | === added file 'cloudinit/sources/DataSourceOpenNebula.py' |
14 | --- cloudinit/sources/DataSourceOpenNebula.py 1970-01-01 00:00:00 +0000 |
15 | +++ cloudinit/sources/DataSourceOpenNebula.py 2013-09-05 16:03:43 +0000 |
16 | @@ -0,0 +1,367 @@ |
17 | +# vi: ts=4 expandtab |
18 | +# |
19 | +# Copyright (C) 2012 Canonical Ltd. |
20 | +# Copyright (C) 2012 Yahoo! Inc. |
21 | +# Copyright (C) 2012-2013 CERIT Scientific Cloud |
22 | +# Copyright (C) 2012-2013 OpenNebula.org |
23 | +# |
24 | +# Author: Scott Moser <scott.moser@canonical.com> |
25 | +# Author: Joshua Harlow <harlowja@yahoo-inc.com> |
26 | +# Author: Vlastimil Holer <xholer@mail.muni.cz> |
27 | +# Author: Javier Fontan <jfontan@opennebula.org> |
28 | +# |
29 | +# This program is free software: you can redistribute it and/or modify |
30 | +# it under the terms of the GNU General Public License version 3, as |
31 | +# published by the Free Software Foundation. |
32 | +# |
33 | +# This program is distributed in the hope that it will be useful, |
34 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
35 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
36 | +# GNU General Public License for more details. |
37 | +# |
38 | +# You should have received a copy of the GNU General Public License |
39 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
40 | + |
41 | +import os |
42 | +import re |
43 | + |
44 | +from cloudinit import log as logging |
45 | +from cloudinit import sources |
46 | +from cloudinit import util |
47 | + |
48 | +LOG = logging.getLogger(__name__) |
49 | + |
50 | +DEFAULT_IID = "iid-dsopennebula" |
51 | +DEFAULT_MODE = 'net' |
52 | +CONTEXT_DISK_FILES = ["context.sh"] |
53 | +VALID_DSMODES = ("local", "net", "disabled") |
54 | + |
55 | + |
56 | +class DataSourceOpenNebula(sources.DataSource): |
57 | + def __init__(self, sys_cfg, distro, paths): |
58 | + sources.DataSource.__init__(self, sys_cfg, distro, paths) |
59 | + self.dsmode = 'local' |
60 | + self.seed = None |
61 | + self.seed_dir = os.path.join(paths.seed_dir, 'opennebula') |
62 | + |
63 | + def __str__(self): |
64 | + root = sources.DataSource.__str__(self) |
65 | + return "%s [seed=%s][dsmode=%s]" % (root, self.seed, self.dsmode) |
66 | + |
67 | + def get_data(self): |
68 | + defaults = { |
69 | + "instance-id": DEFAULT_IID, |
70 | + "dsmode": self.dsmode |
71 | + } |
72 | + |
73 | + seed = None |
74 | + results = {} |
75 | + |
76 | + # first try to read local seed_dir |
77 | + if os.path.isdir(self.seed_dir): |
78 | + try: |
79 | + results = read_context_disk_dir(self.seed_dir) |
80 | + seed = self.seed_dir |
81 | + except NonContextDiskDir: |
82 | + util.logexc(LOG, "Failed reading context from %s", |
83 | + self.seed_dir) |
84 | + |
85 | + if not seed: |
86 | + # then try to detect and mount candidate devices and |
87 | + # read contextualization if present |
88 | + for dev in find_candidate_devs(): |
89 | + try: |
90 | + results = util.mount_cb(dev, read_context_disk_dir) |
91 | + seed = dev |
92 | + break |
93 | + except (NonContextDiskDir, util.MountFailedError): |
94 | + pass |
95 | + |
96 | + if not seed: |
97 | + return False |
98 | + |
99 | + # merge fetched metadata with datasource defaults |
100 | + md = results['metadata'] |
101 | + md = util.mergemanydict([md, defaults]) |
102 | + |
103 | + # check for valid user specified dsmode |
104 | + user_dsmode = results['metadata'].get('dsmode', None) |
105 | + if user_dsmode not in VALID_DSMODES + (None,): |
106 | + LOG.warn("user specified invalid mode: %s", user_dsmode) |
107 | + user_dsmode = None |
108 | + |
109 | + # decide dsmode |
110 | + if user_dsmode: |
111 | + dsmode = user_dsmode |
112 | + elif self.ds_cfg.get('dsmode'): |
113 | + dsmode = self.ds_cfg.get('dsmode') |
114 | + else: |
115 | + dsmode = DEFAULT_MODE |
116 | + |
117 | + if dsmode == "disabled": |
118 | + # most likely user specified |
119 | + return False |
120 | + |
121 | + # apply static network configuration only in 'local' dsmode |
122 | + if ('network-interfaces' in results and self.dsmode == "local"): |
123 | + LOG.debug("Updating network interfaces from %s", self) |
124 | + self.distro.apply_network(results['network-interfaces']) |
125 | + |
126 | + if dsmode != self.dsmode: |
127 | + LOG.debug("%s: not claiming datasource, dsmode=%s", self, dsmode) |
128 | + return False |
129 | + |
130 | + self.seed = seed |
131 | + self.metadata = md |
132 | + self.userdata_raw = results.get('userdata') |
133 | + return True |
134 | + |
135 | + def get_hostname(self, fqdn=False, resolve_ip=None): |
136 | + if resolve_ip is None: |
137 | + if self.dsmode == 'net': |
138 | + resolve_ip = True |
139 | + else: |
140 | + resolve_ip = False |
141 | + return sources.DataSource.get_hostname(self, fqdn, resolve_ip) |
142 | + |
143 | + |
144 | +class DataSourceOpenNebulaNet(DataSourceOpenNebula): |
145 | + def __init__(self, sys_cfg, distro, paths): |
146 | + DataSourceOpenNebula.__init__(self, sys_cfg, distro, paths) |
147 | + self.dsmode = 'net' |
148 | + |
149 | + |
150 | +class NonContextDiskDir(Exception): |
151 | + pass |
152 | + |
153 | + |
154 | +class OpenNebulaNetwork(object): |
155 | + REG_DEV_MAC = re.compile( |
156 | + r'^\d+: (eth\d+):.*?link\/ether (..:..:..:..:..:..) ?', |
157 | + re.MULTILINE | re.DOTALL) |
158 | + |
159 | + def __init__(self, ip, context): |
160 | + self.ip = ip |
161 | + self.context = context |
162 | + self.ifaces = self.get_ifaces() |
163 | + |
164 | + def get_ifaces(self): |
165 | + return self.REG_DEV_MAC.findall(self.ip) |
166 | + |
167 | + def mac2ip(self, mac): |
168 | + components = mac.split(':')[2:] |
169 | + return [str(int(c, 16)) for c in components] |
170 | + |
171 | + def get_ip(self, dev, components): |
172 | + var_name = dev + '_ip' |
173 | + if var_name in self.context: |
174 | + return self.context[var_name] |
175 | + else: |
176 | + return '.'.join(components) |
177 | + |
178 | + def get_mask(self, dev): |
179 | + var_name = dev + '_mask' |
180 | + if var_name in self.context: |
181 | + return self.context[var_name] |
182 | + else: |
183 | + return '255.255.255.0' |
184 | + |
185 | + def get_network(self, dev, components): |
186 | + var_name = dev + '_network' |
187 | + if var_name in self.context: |
188 | + return self.context[var_name] |
189 | + else: |
190 | + return '.'.join(components[:-1]) + '.0' |
191 | + |
192 | + def get_gateway(self, dev): |
193 | + var_name = dev + '_gateway' |
194 | + if var_name in self.context: |
195 | + return self.context[var_name] |
196 | + else: |
197 | + return None |
198 | + |
199 | + def get_dns(self, dev): |
200 | + var_name = dev + '_dns' |
201 | + if var_name in self.context: |
202 | + return self.context[var_name] |
203 | + else: |
204 | + return None |
205 | + |
206 | + def get_domain(self, dev): |
207 | + var_name = dev + '_domain' |
208 | + if var_name in self.context: |
209 | + return self.context[var_name] |
210 | + else: |
211 | + return None |
212 | + |
213 | + def gen_conf(self): |
214 | + global_dns = [] |
215 | + if 'dns' in self.context: |
216 | + global_dns.append(self.context['dns']) |
217 | + |
218 | + conf = [] |
219 | + conf.append('auto lo') |
220 | + conf.append('iface lo inet loopback') |
221 | + conf.append('') |
222 | + |
223 | + for i in self.ifaces: |
224 | + dev = i[0] |
225 | + mac = i[1] |
226 | + ip_components = self.mac2ip(mac) |
227 | + |
228 | + conf.append('auto ' + dev) |
229 | + conf.append('iface ' + dev + ' inet static') |
230 | + conf.append(' address ' + self.get_ip(dev, ip_components)) |
231 | + conf.append(' network ' + self.get_network(dev, ip_components)) |
232 | + conf.append(' netmask ' + self.get_mask(dev)) |
233 | + |
234 | + gateway = self.get_gateway(dev) |
235 | + if gateway: |
236 | + conf.append(' gateway ' + gateway) |
237 | + |
238 | + domain = self.get_domain(dev) |
239 | + if domain: |
240 | + conf.append(' dns-search ' + domain) |
241 | + |
242 | + # add global DNS servers to all interfaces |
243 | + dns = self.get_dns(dev) |
244 | + if global_dns or dns: |
245 | + all_dns = global_dns |
246 | + if dns: |
247 | + all_dns.append(dns) |
248 | + conf.append(' dns-nameservers ' + ' '.join(all_dns)) |
249 | + |
250 | + conf.append('') |
251 | + |
252 | + return "\n".join(conf) |
253 | + |
254 | + |
255 | +def find_candidate_devs(): |
256 | + """ |
257 | + Return a list of devices that may contain the context disk. |
258 | + """ |
259 | + combined = [] |
260 | + for f in ('LABEL=CONTEXT', 'LABEL=CDROM', 'TYPE=iso9660'): |
261 | + devs = util.find_devs_with(f) |
262 | + devs.sort() |
263 | + for d in devs: |
264 | + if d not in combined: |
265 | + combined.append(d) |
266 | + |
267 | + return combined |
268 | + |
269 | + |
270 | +def parse_context_data(data): |
271 | + """ |
272 | + parse_context_data(data) |
273 | + parse context.sh variables provided as a single string. Uses |
274 | + very simple matching RE. Returns None if nothing is matched. |
275 | + """ |
276 | + # RE groups: |
277 | + # 1: key |
278 | + # 2: single quoted value, respect '\'' |
279 | + # 3: old double quoted value, but doesn't end with \" |
280 | + context_reg = re.compile( |
281 | + r"^([\w_]+)=(?:'((?:[^']|'\\'')*?)'|\"(.*?[^\\])\")$", |
282 | + re.MULTILINE | re.DOTALL) |
283 | + |
284 | + found = context_reg.findall(data) |
285 | + if not found: |
286 | + return None |
287 | + |
288 | + variables = {} |
289 | + for k, v1, v2 in found: |
290 | + k = k.lower() |
291 | + if v1: |
292 | + # take single quoted variable 'xyz' |
293 | + # (ON>=4) and unquote '\'' -> ' |
294 | + variables[k] = v1.replace(r"'\''", r"'") |
295 | + elif v2: |
296 | + # take double quoted variable "xyz" |
297 | + # (old ON<4) and unquote \" -> " |
298 | + variables[k] = v2.replace(r'\"', r'"') |
299 | + |
300 | + return variables |
301 | + |
302 | + |
303 | +def read_context_disk_dir(source_dir): |
304 | + """ |
305 | + read_context_disk_dir(source_dir): |
306 | + read source_dir and return a tuple with metadata dict and user-data |
307 | + string populated. If not a valid dir, raise a NonContextDiskDir |
308 | + """ |
309 | + found = {} |
310 | + for af in CONTEXT_DISK_FILES: |
311 | + fn = os.path.join(source_dir, af) |
312 | + if os.path.isfile(fn): |
313 | + found[af] = fn |
314 | + |
315 | + if not found: |
316 | + raise NonContextDiskDir("%s: %s" % (source_dir, "no files found")) |
317 | + |
318 | + results = {'userdata': None, 'metadata': {}} |
319 | + context = {} |
320 | + |
321 | + if "context.sh" in found: |
322 | + try: |
323 | + with open(os.path.join(source_dir, 'context.sh'), 'r') as f: |
324 | + context = parse_context_data(f.read()) |
325 | + f.close() |
326 | + if not context: |
327 | + raise NonContextDiskDir("No variables in context") |
328 | + |
329 | + except (IOError, NonContextDiskDir) as e: |
330 | + raise NonContextDiskDir("Error reading context.sh: %s" % (e)) |
331 | + |
332 | + results['metadata'] = context |
333 | + else: |
334 | + raise NonContextDiskDir("Missing context.sh") |
335 | + |
336 | + # process single or multiple SSH keys |
337 | + ssh_key_var = None |
338 | + if "ssh_key" in context: |
339 | + ssh_key_var = "ssh_key" |
340 | + elif "ssh_public_key" in context: |
341 | + ssh_key_var = "ssh_public_key" |
342 | + |
343 | + if ssh_key_var: |
344 | + lines = context.get(ssh_key_var).splitlines() |
345 | + results['metadata']['public-keys'] = [l for l in lines |
346 | + if len(l) and not l.startswith("#")] |
347 | + |
348 | + # custom hostname -- try hostname or leave cloud-init |
349 | + # itself create hostname from IP address later |
350 | + for k in ('hostname', 'public_ip', 'ip_public', 'eth0_ip'): |
351 | + if k in context: |
352 | + results['metadata']['local-hostname'] = context[k] |
353 | + break |
354 | + |
355 | + # raw user data |
356 | + if "user_data" in context: |
357 | + results['userdata'] = context["user_data"] |
358 | + elif "userdata" in context: |
359 | + results['userdata'] = context["userdata"] |
360 | + |
361 | + # generate static /etc/network/interfaces |
362 | + # only if there are any required context variables |
363 | + # http://opennebula.org/documentation:rel3.8:cong#network_configuration |
364 | + for k in context.keys(): |
365 | + if re.match(r'^eth\d+_ip$', k): |
366 | + (out, _) = util.subp(['/sbin/ip', 'link']) |
367 | + net = OpenNebulaNetwork(out, context) |
368 | + results['network-interfaces'] = net.gen_conf() |
369 | + break |
370 | + |
371 | + return results |
372 | + |
373 | + |
374 | +# Used to match classes to dependencies |
375 | +datasources = [ |
376 | + (DataSourceOpenNebula, (sources.DEP_FILESYSTEM, )), |
377 | + (DataSourceOpenNebulaNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), |
378 | +] |
379 | + |
380 | + |
381 | +# Return a list of data sources that match this set of dependencies |
382 | +def get_datasource_list(depends): |
383 | + return sources.list_from_depends(depends, datasources) |
384 | |
385 | === modified file 'cloudinit/sources/__init__.py' |
386 | --- cloudinit/sources/__init__.py 2013-07-23 17:10:33 +0000 |
387 | +++ cloudinit/sources/__init__.py 2013-09-05 16:03:43 +0000 |
388 | @@ -144,7 +144,7 @@ |
389 | return "iid-datasource" |
390 | return str(self.metadata['instance-id']) |
391 | |
392 | - def get_hostname(self, fqdn=False): |
393 | + def get_hostname(self, fqdn=False, resolve_ip=False): |
394 | defdomain = "localdomain" |
395 | defhost = "localhost" |
396 | domain = defdomain |
397 | @@ -168,7 +168,14 @@ |
398 | # make up a hostname (LP: #475354) in format ip-xx.xx.xx.xx |
399 | lhost = self.metadata['local-hostname'] |
400 | if util.is_ipv4(lhost): |
401 | - toks = ["ip-%s" % lhost.replace(".", "-")] |
402 | + toks = [] |
403 | + if resolve_ip: |
404 | + toks = util.gethostbyaddr(lhost) |
405 | + |
406 | + if toks: |
407 | + toks = toks.split('.') |
408 | + else: |
409 | + toks = ["ip-%s" % lhost.replace(".", "-")] |
410 | else: |
411 | toks = lhost.split(".") |
412 | |
413 | |
414 | === modified file 'cloudinit/util.py' |
415 | --- cloudinit/util.py 2013-07-30 18:28:09 +0000 |
416 | +++ cloudinit/util.py 2013-09-05 16:03:43 +0000 |
417 | @@ -955,6 +955,13 @@ |
418 | return hostname |
419 | |
420 | |
421 | +def gethostbyaddr(ip): |
422 | + try: |
423 | + return socket.gethostbyaddr(ip)[0] |
424 | + except socket.herror: |
425 | + return None |
426 | + |
427 | + |
428 | def is_resolvable_url(url): |
429 | """determine if this url is resolvable (existing or ip).""" |
430 | return (is_resolvable(urlparse.urlparse(url).hostname)) |
431 | |
432 | === modified file 'doc/rtd/topics/datasources.rst' |
433 | --- doc/rtd/topics/datasources.rst 2013-02-06 07:58:49 +0000 |
434 | +++ doc/rtd/topics/datasources.rst 2013-09-05 16:03:43 +0000 |
435 | @@ -141,6 +141,12 @@ |
436 | .. include:: ../../sources/configdrive/README.rst |
437 | |
438 | --------------------------- |
439 | +OpenNebula |
440 | +--------------------------- |
441 | + |
442 | +.. include:: ../../sources/opennebula/README.rst |
443 | + |
444 | +--------------------------- |
445 | Alt cloud |
446 | --------------------------- |
447 | |
448 | |
449 | === added directory 'doc/sources/opennebula' |
450 | === added file 'doc/sources/opennebula/README.rst' |
451 | --- doc/sources/opennebula/README.rst 1970-01-01 00:00:00 +0000 |
452 | +++ doc/sources/opennebula/README.rst 2013-09-05 16:03:43 +0000 |
453 | @@ -0,0 +1,132 @@ |
454 | +The `OpenNebula`_ (ON) datasource supports the contextualization disk. |
455 | + |
456 | + See `contextualization overview`_, `contextualizing VMs`_ and |
457 | + `network configuration`_ in the public documentation for |
458 | + more information. |
459 | + |
460 | +OpenNebula's virtual machines are contextualized (parametrized) by |
461 | +CD-ROM image, which contains a shell script *context.sh* with |
462 | +custom variables defined on virtual machine start. There are no |
463 | +fixed contextualization variables, but the datasource accepts |
464 | +many used and recommended across the documentation. |
465 | + |
466 | +Datasource configuration |
467 | +~~~~~~~~~~~~~~~~~~~~~~~~~ |
468 | + |
469 | +Datasource accepts following configuration options. |
470 | + |
471 | +:: |
472 | + |
473 | + dsmode: |
474 | + values: local, net, disabled |
475 | + default: net |
476 | + |
477 | +Tells if this datasource will be processed in 'local' (pre-networking) or |
478 | +'net' (post-networking) stage or even completely 'disabled'. |
479 | + |
480 | +Contextualization disk |
481 | +~~~~~~~~~~~~~~~~~~~~~~ |
482 | + |
483 | +The following criteria are required: |
484 | + |
485 | +1. Must be formatted with `iso9660`_ fs. or have fs. label of **CDROM** |
486 | +2. Must contain file *context.sh* with contextualization variables. |
487 | + File is generated by OpenNebula, it has a KEY="VALUE" format and |
488 | + can be easily read (via *source*) by shell |
489 | + |
490 | +Contextualization variables |
491 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
492 | + |
493 | +There are no fixed contextualization variables in OpenNebula, no standard. |
494 | +Following variables were found on various places and revisions of |
495 | +the OpenNebula documentation. Where multiple similar variables are |
496 | +specified, only first found is taken. |
497 | + |
498 | +:: |
499 | + |
500 | + DSMODE |
501 | + |
502 | +Datasource mode configuration override. Values: local, net, disabled. |
503 | + |
504 | +:: |
505 | + |
506 | + DNS |
507 | + ETH<x>_IP |
508 | + ETH<x>_NETWORK |
509 | + ETH<x>_MASK |
510 | + ETH<x>_GATEWAY |
511 | + ETH<x>_DOMAIN |
512 | + ETH<x>_DNS |
513 | + |
514 | +Static `network configuration`_. |
515 | + |
516 | +:: |
517 | + |
518 | + HOSTNAME |
519 | + |
520 | +Instance hostname. |
521 | + |
522 | +:: |
523 | + |
524 | + PUBLIC_IP |
525 | + IP_PUBLIC |
526 | + ETH0_IP |
527 | + |
528 | +If no hostname has been specified, cloud-init will try to create hostname |
529 | +from instance's IP address in 'local' dsmode. In 'net' dsmode, cloud-init |
530 | +tries to resolve one of its IP addresses to get hostname. |
531 | + |
532 | +:: |
533 | + |
534 | + SSH_KEY |
535 | + SSH_PUBLIC_KEY |
536 | + |
537 | +One or multiple SSH keys (separated by newlines) can be specified. |
538 | + |
539 | +:: |
540 | + |
541 | + USER_DATA |
542 | + USERDATA |
543 | + |
544 | +cloud-init user data. |
545 | + |
546 | +Example configuration |
547 | +~~~~~~~~~~~~~~~~~~~~~ |
548 | + |
549 | +This example cloud-init configuration (*cloud.cfg*) enables |
550 | +OpenNebula datasource only in 'net' mode. |
551 | + |
552 | +:: |
553 | + |
554 | + disable_ec2_metadata: True |
555 | + datasource_list: ['OpenNebula'] |
556 | + datasource: |
557 | + OpenNebula: |
558 | + dsmode: net |
559 | + |
560 | +Example VM's context section |
561 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
562 | + |
563 | +:: |
564 | + |
565 | + CONTEXT=[ |
566 | + PUBLIC_IP="$NIC[IP]", |
567 | + SSH_KEY="$USER[SSH_KEY] |
568 | + $USER[SSH_KEY1] |
569 | + $USER[SSH_KEY2] ", |
570 | + USER_DATA="#cloud-config |
571 | + # see https://help.ubuntu.com/community/CloudInit |
572 | + |
573 | + packages: [] |
574 | + |
575 | + mounts: |
576 | + - [vdc,none,swap,sw,0,0] |
577 | + runcmd: |
578 | + - echo 'Instance has been configured by cloud-init.' | wall |
579 | + " ] |
580 | + |
581 | +.. _OpenNebula: http://opennebula.org/ |
582 | +.. _contextualization overview: http://opennebula.org/documentation:documentation:context_overview |
583 | +.. _contextualizing VMs: http://opennebula.org/documentation:documentation:cong |
584 | +.. _network configuration: http://opennebula.org/documentation:documentation:cong#network_configuration |
585 | +.. _iso9660: https://en.wikipedia.org/wiki/ISO_9660 |
586 | |
587 | === added file 'tests/unittests/test_datasource/test_opennebula.py' |
588 | --- tests/unittests/test_datasource/test_opennebula.py 1970-01-01 00:00:00 +0000 |
589 | +++ tests/unittests/test_datasource/test_opennebula.py 2013-09-05 16:03:43 +0000 |
590 | @@ -0,0 +1,169 @@ |
591 | +from cloudinit.sources import DataSourceOpenNebula as ds |
592 | +from cloudinit import util |
593 | +from mocker import MockerTestCase |
594 | +from tests.unittests.helpers import populate_dir |
595 | + |
596 | +import os |
597 | + |
598 | +TEST_VARS = { |
599 | + 'var1': 'single', |
600 | + 'var2': 'double word', |
601 | + 'var3': 'multi\nline\n', |
602 | + 'var4': "'single'", |
603 | + 'var5': "'double word'", |
604 | + 'var6': "'multi\nline\n'", |
605 | + 'var7': 'single\\t', |
606 | + 'var8': 'double\\tword', |
607 | + 'var9': 'multi\\t\nline\n', |
608 | + 'var10': '\\', # expect \ |
609 | + 'var11': '\'', # expect ' |
610 | + 'var12': '$', # expect $ |
611 | +} |
612 | + |
613 | +USER_DATA = '#cloud-config\napt_upgrade: true' |
614 | +SSH_KEY = 'ssh-rsa AAAAB3NzaC1....sIkJhq8wdX+4I3A4cYbYP ubuntu@server-460-%i' |
615 | +HOSTNAME = 'foo.example.com' |
616 | +PUBLIC_IP = '10.0.0.3' |
617 | + |
618 | +CMD_IP_OUT = '''\ |
619 | +1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN |
620 | + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 |
621 | +2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000 |
622 | + link/ether 02:00:0a:12:01:01 brd ff:ff:ff:ff:ff:ff |
623 | +''' |
624 | + |
625 | + |
626 | +class TestOpenNebulaDataSource(MockerTestCase): |
627 | + |
628 | + def setUp(self): |
629 | + super(TestOpenNebulaDataSource, self).setUp() |
630 | + self.tmp = self.makeDir() |
631 | + |
632 | + def test_seed_dir_non_contextdisk(self): |
633 | + my_d = os.path.join(self.tmp, 'non-contextdisk') |
634 | + self.assertRaises(ds.NonContextDiskDir, ds.read_context_disk_dir, my_d) |
635 | + |
636 | + def test_seed_dir_bad_context(self): |
637 | + my_d = os.path.join(self.tmp, 'bad-context') |
638 | + os.mkdir(my_d) |
639 | + open(os.path.join(my_d, "context.sh"), "w").close() |
640 | + self.assertRaises(ds.NonContextDiskDir, ds.read_context_disk_dir, my_d) |
641 | + |
642 | + def test_context_parser(self): |
643 | + my_d = os.path.join(self.tmp, 'context-parser') |
644 | + populate_context_dir(my_d, TEST_VARS) |
645 | + results = ds.read_context_disk_dir(my_d) |
646 | + |
647 | + self.assertTrue('metadata' in results) |
648 | + self.assertEqual(TEST_VARS, results['metadata']) |
649 | + |
650 | + def test_ssh_key(self): |
651 | + public_keys = ['first key', 'second key'] |
652 | + for c in range(4): |
653 | + for k in ('SSH_KEY', 'SSH_PUBLIC_KEY'): |
654 | + my_d = os.path.join(self.tmp, "%s-%i" % (k, c)) |
655 | + populate_context_dir(my_d, {k: '\n'.join(public_keys)}) |
656 | + results = ds.read_context_disk_dir(my_d) |
657 | + |
658 | + self.assertTrue('metadata' in results) |
659 | + self.assertTrue('public-keys' in results['metadata']) |
660 | + self.assertEqual(public_keys, |
661 | + results['metadata']['public-keys']) |
662 | + |
663 | + public_keys.append(SSH_KEY % (c + 1,)) |
664 | + |
665 | + def test_user_data(self): |
666 | + for k in ('USER_DATA', 'USERDATA'): |
667 | + my_d = os.path.join(self.tmp, k) |
668 | + populate_context_dir(my_d, {k: USER_DATA}) |
669 | + results = ds.read_context_disk_dir(my_d) |
670 | + |
671 | + self.assertTrue('userdata' in results) |
672 | + self.assertEqual(USER_DATA, results['userdata']) |
673 | + |
674 | + def test_hostname(self): |
675 | + for k in ('HOSTNAME', 'PUBLIC_IP', 'IP_PUBLIC', 'ETH0_IP'): |
676 | + my_d = os.path.join(self.tmp, k) |
677 | + populate_context_dir(my_d, {k: PUBLIC_IP}) |
678 | + results = ds.read_context_disk_dir(my_d) |
679 | + |
680 | + self.assertTrue('metadata' in results) |
681 | + self.assertTrue('local-hostname' in results['metadata']) |
682 | + self.assertEqual(PUBLIC_IP, results['metadata']['local-hostname']) |
683 | + |
684 | + def test_find_candidates(self): |
685 | + def my_devs_with(criteria): |
686 | + return { |
687 | + "LABEL=CONTEXT": ["/dev/sdb"], |
688 | + "LABEL=CDROM": ["/dev/sr0"], |
689 | + "TYPE=iso9660": ["/dev/vdb"], |
690 | + }.get(criteria, []) |
691 | + |
692 | + try: |
693 | + orig_find_devs_with = util.find_devs_with |
694 | + util.find_devs_with = my_devs_with |
695 | + self.assertEqual(["/dev/sdb", "/dev/sr0", "/dev/vdb"], |
696 | + ds.find_candidate_devs()) |
697 | + finally: |
698 | + util.find_devs_with = orig_find_devs_with |
699 | + |
700 | + |
701 | +class TestOpenNebulaNetwork(MockerTestCase): |
702 | + |
703 | + def setUp(self): |
704 | + super(TestOpenNebulaNetwork, self).setUp() |
705 | + |
706 | + def test_lo(self): |
707 | + net = ds.OpenNebulaNetwork('', {}) |
708 | + self.assertEqual(net.gen_conf(), u'''\ |
709 | +auto lo |
710 | +iface lo inet loopback |
711 | +''') |
712 | + |
713 | + def test_eth0(self): |
714 | + net = ds.OpenNebulaNetwork(CMD_IP_OUT, {}) |
715 | + self.assertEqual(net.gen_conf(), u'''\ |
716 | +auto lo |
717 | +iface lo inet loopback |
718 | + |
719 | +auto eth0 |
720 | +iface eth0 inet static |
721 | + address 10.18.1.1 |
722 | + network 10.18.1.0 |
723 | + netmask 255.255.255.0 |
724 | +''') |
725 | + |
726 | + def test_eth0_override(self): |
727 | + context = { |
728 | + 'dns': '1.2.3.8', |
729 | + 'eth0_ip': '1.2.3.4', |
730 | + 'eth0_network': '1.2.3.0', |
731 | + 'eth0_mask': '255.255.0.0', |
732 | + 'eth0_gateway': '1.2.3.5', |
733 | + 'eth0_domain': 'example.com', |
734 | + 'eth0_dns': '1.2.3.6 1.2.3.7' |
735 | + } |
736 | + |
737 | + net = ds.OpenNebulaNetwork(CMD_IP_OUT, context) |
738 | + self.assertEqual(net.gen_conf(), u'''\ |
739 | +auto lo |
740 | +iface lo inet loopback |
741 | + |
742 | +auto eth0 |
743 | +iface eth0 inet static |
744 | + address 1.2.3.4 |
745 | + network 1.2.3.0 |
746 | + netmask 255.255.0.0 |
747 | + gateway 1.2.3.5 |
748 | + dns-search example.com |
749 | + dns-nameservers 1.2.3.8 1.2.3.6 1.2.3.7 |
750 | +''') |
751 | + |
752 | + |
753 | +def populate_context_dir(path, variables): |
754 | + data = "# Context variables generated by OpenNebula\n" |
755 | + for (k, v) in variables.iteritems(): |
756 | + data += ("%s='%s'\n" % (k.upper(), v.replace(r"'", r"'\''"))) |
757 | + populate_dir(path, {'context.sh': data}) |
758 | + |
759 | +# vi: ts=4 expandtab |
Just a few comments, otherwise seems cool.
Is 'NonConfigDriveDir' (exception afaik) supposed to be brought in somewhere?
Should this ds class be derived from the config drive datasource?
- This is due to the new overload that was just added there
for mapping device names to actual devices. I noticed this
code does not have that function either (without it fstab
will not be adjusted). So perhaps its better to inherit
if the functionality is the same?? Perhaps override getdata()
but still use the rest of the parent?
For capturing 'except subprocess. CalledProcessEr ror as exc:' you might want to catch the one in 'util.py' instead (its not the same exception - although maybe it should be).
Is it possible add more comments as to what the context.sh parsing is doing? PIPESTATUS| _)="'' (???)
Ie: ''comm -23 <(set | sort -u) <(echo "$VARS") | egrep -v "^(VARS|
context_ sh[key. lower() ]=r.group( 1).\ '\\\\', '\\').\ '\\t',' \t').\ '\\n',' \n').\
205 + replace(
206 + replace(
207 + replace(
208 + replace("\\'","'")
Should this just be re.group( 1).decode( 'string_ escape' )? 'string_ escape' )
>>> print c
\n\tblahblah\n\'
>>> print c.decode(
blahblah
'