diff options
-rw-r--r-- | ChangeLog | 1 | ||||
-rw-r--r-- | cloudinit/settings.py | 1 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceOpenNebula.py | 442 | ||||
-rw-r--r-- | cloudinit/sources/__init__.py | 18 | ||||
-rw-r--r-- | cloudinit/util.py | 7 | ||||
-rw-r--r-- | doc/rtd/topics/datasources.rst | 6 | ||||
-rw-r--r-- | doc/sources/opennebula/README.rst | 142 | ||||
-rw-r--r-- | tests/unittests/test_datasource/test_opennebula.py | 267 |
8 files changed, 882 insertions, 2 deletions
@@ -17,6 +17,7 @@ - prefer growpart resizer to 'parted resizepart' (LP: #1212492) - support random data seed from config drive or azure, and a module 'seed_random' to read that and write it to /dev/urandom. + - add OpenNebula Datasource [Vlastimil Holer] 0.7.2: - add a debian watch file - add 'sudo' entry to ubuntu's default user (LP: #1080717) diff --git a/cloudinit/settings.py b/cloudinit/settings.py index 9f6badae..5df7f557 100644 --- a/cloudinit/settings.py +++ b/cloudinit/settings.py @@ -31,6 +31,7 @@ CFG_BUILTIN = { 'datasource_list': [ 'NoCloud', 'ConfigDrive', + 'OpenNebula', 'Azure', 'AltCloud', 'OVF', diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py new file mode 100644 index 00000000..07dc25ff --- /dev/null +++ b/cloudinit/sources/DataSourceOpenNebula.py @@ -0,0 +1,442 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Canonical Ltd. +# Copyright (C) 2012 Yahoo! Inc. +# Copyright (C) 2012-2013 CERIT Scientific Cloud +# Copyright (C) 2012-2013 OpenNebula.org +# +# Author: Scott Moser <scott.moser@canonical.com> +# Author: Joshua Harlow <harlowja@yahoo-inc.com> +# Author: Vlastimil Holer <xholer@mail.muni.cz> +# Author: Javier Fontan <jfontan@opennebula.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import pwd +import re +import string # pylint: disable=W0402 + +from cloudinit import log as logging +from cloudinit import sources +from cloudinit import util + +LOG = logging.getLogger(__name__) + +DEFAULT_IID = "iid-dsopennebula" +DEFAULT_MODE = 'net' +DEFAULT_PARSEUSER = 'nobody' +CONTEXT_DISK_FILES = ["context.sh"] +VALID_DSMODES = ("local", "net", "disabled") + + +class DataSourceOpenNebula(sources.DataSource): + def __init__(self, sys_cfg, distro, paths): + sources.DataSource.__init__(self, sys_cfg, distro, paths) + self.dsmode = 'local' + self.seed = None + self.seed_dir = os.path.join(paths.seed_dir, 'opennebula') + + def __str__(self): + root = sources.DataSource.__str__(self) + return "%s [seed=%s][dsmode=%s]" % (root, self.seed, self.dsmode) + + def get_data(self): + defaults = {"instance-id": DEFAULT_IID} + results = None + seed = None + + # decide parseuser for context.sh shell reader + parseuser = DEFAULT_PARSEUSER + if 'parseuser' in self.ds_cfg: + parseuser = self.ds_cfg.get('parseuser') + + candidates = [self.seed_dir] + candidates.extend(find_candidate_devs()) + for cdev in candidates: + try: + if os.path.isdir(self.seed_dir): + results = read_context_disk_dir(cdev, asuser=parseuser) + elif cdev.startswith("/dev"): + results = util.mount_cb(cdev, read_context_disk_dir, + data=parseuser) + except NonContextDiskDir: + continue + except BrokenContextDiskDir as exc: + raise exc + except util.MountFailedError: + LOG.warn("%s was not mountable" % cdev) + + if results: + seed = cdev + LOG.debug("found datasource in %s", cdev) + break + + if not seed: + return False + + # merge fetched metadata with datasource defaults + md = results['metadata'] + md = util.mergemanydict([md, defaults]) + + # check for valid user specified dsmode + user_dsmode = results['metadata'].get('DSMODE', None) + if user_dsmode not in VALID_DSMODES + (None,): + LOG.warn("user specified invalid mode: %s", user_dsmode) + user_dsmode = None + + # decide dsmode + if user_dsmode: + dsmode = user_dsmode + elif self.ds_cfg.get('dsmode'): + dsmode = self.ds_cfg.get('dsmode') + else: + dsmode = DEFAULT_MODE + + if dsmode == "disabled": + # most likely user specified + return False + + # apply static network configuration only in 'local' dsmode + if ('network-interfaces' in results and self.dsmode == "local"): + LOG.debug("Updating network interfaces from %s", self) + self.distro.apply_network(results['network-interfaces']) + + if dsmode != self.dsmode: + LOG.debug("%s: not claiming datasource, dsmode=%s", self, dsmode) + return False + + self.seed = seed + self.metadata = md + self.userdata_raw = results.get('userdata') + return True + + def get_hostname(self, fqdn=False, resolve_ip=None): + if resolve_ip is None: + if self.dsmode == 'net': + resolve_ip = True + else: + resolve_ip = False + return sources.DataSource.get_hostname(self, fqdn, resolve_ip) + + +class DataSourceOpenNebulaNet(DataSourceOpenNebula): + def __init__(self, sys_cfg, distro, paths): + DataSourceOpenNebula.__init__(self, sys_cfg, distro, paths) + self.dsmode = 'net' + + +class NonContextDiskDir(Exception): + pass + + +class BrokenContextDiskDir(Exception): + pass + + +class OpenNebulaNetwork(object): + REG_DEV_MAC = re.compile( + r'^\d+: (eth\d+):.*?link\/ether (..:..:..:..:..:..) ?', + re.MULTILINE | re.DOTALL) + + def __init__(self, ip, context): + self.ip = ip + self.context = context + self.ifaces = self.get_ifaces() + + def get_ifaces(self): + return self.REG_DEV_MAC.findall(self.ip) + + def mac2ip(self, mac): + components = mac.split(':')[2:] + return [str(int(c, 16)) for c in components] + + def get_ip(self, dev, components): + var_name = dev.upper() + '_IP' + if var_name in self.context: + return self.context[var_name] + else: + return '.'.join(components) + + def get_mask(self, dev): + var_name = dev.upper() + '_MASK' + if var_name in self.context: + return self.context[var_name] + else: + return '255.255.255.0' + + def get_network(self, dev, components): + var_name = dev.upper() + '_NETWORK' + if var_name in self.context: + return self.context[var_name] + else: + return '.'.join(components[:-1]) + '.0' + + def get_gateway(self, dev): + var_name = dev.upper() + '_GATEWAY' + if var_name in self.context: + return self.context[var_name] + else: + return None + + def get_dns(self, dev): + var_name = dev.upper() + '_DNS' + if var_name in self.context: + return self.context[var_name] + else: + return None + + def get_domain(self, dev): + var_name = dev.upper() + '_DOMAIN' + if var_name in self.context: + return self.context[var_name] + else: + return None + + def gen_conf(self): + global_dns = [] + if 'DNS' in self.context: + global_dns.append(self.context['DNS']) + + conf = [] + conf.append('auto lo') + conf.append('iface lo inet loopback') + conf.append('') + + for i in self.ifaces: + dev = i[0] + mac = i[1] + ip_components = self.mac2ip(mac) + + conf.append('auto ' + dev) + conf.append('iface ' + dev + ' inet static') + conf.append(' address ' + self.get_ip(dev, ip_components)) + conf.append(' network ' + self.get_network(dev, ip_components)) + conf.append(' netmask ' + self.get_mask(dev)) + + gateway = self.get_gateway(dev) + if gateway: + conf.append(' gateway ' + gateway) + + domain = self.get_domain(dev) + if domain: + conf.append(' dns-search ' + domain) + + # add global DNS servers to all interfaces + dns = self.get_dns(dev) + if global_dns or dns: + all_dns = global_dns + if dns: + all_dns.append(dns) + conf.append(' dns-nameservers ' + ' '.join(all_dns)) + + conf.append('') + + return "\n".join(conf) + + +def find_candidate_devs(): + """ + Return a list of devices that may contain the context disk. + """ + combined = [] + for f in ('LABEL=CONTEXT', 'LABEL=CDROM', 'TYPE=iso9660'): + devs = util.find_devs_with(f) + devs.sort() + for d in devs: + if d not in combined: + combined.append(d) + + return combined + + +def switch_user_cmd(user): + return ['sudo', '-u', user] + + +def parse_shell_config(content, keylist=None, bash=None, asuser=None, + switch_user_cb=None): + + if isinstance(bash, str): + bash = [bash] + elif bash is None: + bash = ['bash', '-e'] + + if switch_user_cb is None: + switch_user_cb = switch_user_cmd + + # allvars expands to all existing variables by using '${!x*}' notation + # where x is lower or upper case letters or '_' + allvars = ["${!%s*}" % x for x in string.letters + "_"] + + keylist_in = keylist + if keylist is None: + keylist = allvars + keylist_in = [] + + setup = '\n'.join(('__v="";', '',)) + + def varprinter(vlist): + # output '\0'.join(['_start_', key=value NULL for vars in vlist] + return '\n'.join(( + 'printf "%s\\0" _start_', + 'for __v in %s; do' % ' '.join(vlist), + ' printf "%s=%s\\0" "$__v" "${!__v}";', + 'done', + '' + )) + + # the rendered 'bcmd' is bash syntax that does + # setup: declare variables we use (so they show up in 'all') + # varprinter(allvars): print all variables known at beginning + # content: execute the provided content + # varprinter(keylist): print all variables known after content + # + # output is then a null terminated array of: + # literal '_start_' + # key=value (for each preset variable) + # literal '_start_' + # key=value (for each post set variable) + bcmd = ('unset IFS\n' + + setup + + varprinter(allvars) + + '{\n%s\n\n:\n} > /dev/null\n' % content + + 'unset IFS\n' + + varprinter(keylist) + "\n") + + cmd = [] + if asuser is not None: + cmd = switch_user_cb(asuser) + + cmd.extend(bash) + + (output, _error) = util.subp(cmd, data=bcmd) + + # exclude vars in bash that change on their own or that we used + excluded = ("RANDOM", "LINENO", "_", "__v") + preset = {} + ret = {} + target = None + output = output[0:-1] # remove trailing null + + # go through output. First _start_ is for 'preset', second for 'target'. + # Add to target only things were changed and not in volitile + for line in output.split("\x00"): + try: + (key, val) = line.split("=", 1) + if target is preset: + target[key] = val + elif (key not in excluded and + (key in keylist_in or preset.get(key) != val)): + ret[key] = val + except ValueError: + if line != "_start_": + raise + if target is None: + target = preset + elif target is preset: + target = ret + + return ret + + +def read_context_disk_dir(source_dir, asuser=None): + """ + read_context_disk_dir(source_dir): + read source_dir and return a tuple with metadata dict and user-data + string populated. If not a valid dir, raise a NonContextDiskDir + """ + found = {} + for af in CONTEXT_DISK_FILES: + fn = os.path.join(source_dir, af) + if os.path.isfile(fn): + found[af] = fn + + if not found: + raise NonContextDiskDir("%s: %s" % (source_dir, "no files found")) + + context = {} + results = {'userdata': None, 'metadata': {}} + + if "context.sh" in found: + if asuser is not None: + try: + pwd.getpwnam(asuser) + except KeyError as e: + raise BrokenContextDiskDir("configured user '%s' " + "does not exist", asuser) + try: + with open(os.path.join(source_dir, 'context.sh'), 'r') as f: + content = f.read().strip() + + context = parse_shell_config(content, asuser=asuser) + except util.ProcessExecutionError as e: + raise BrokenContextDiskDir("Error processing context.sh: %s" % (e)) + except IOError as e: + raise NonContextDiskDir("Error reading context.sh: %s" % (e)) + else: + raise NonContextDiskDir("Missing context.sh") + + if not context: + return results + + results['metadata'] = context + + # process single or multiple SSH keys + ssh_key_var = None + if "SSH_KEY" in context: + ssh_key_var = "SSH_KEY" + elif "SSH_PUBLIC_KEY" in context: + ssh_key_var = "SSH_PUBLIC_KEY" + + if ssh_key_var: + lines = context.get(ssh_key_var).splitlines() + results['metadata']['public-keys'] = [l for l in lines + if len(l) and not l.startswith("#")] + + # custom hostname -- try hostname or leave cloud-init + # itself create hostname from IP address later + for k in ('HOSTNAME', 'PUBLIC_IP', 'IP_PUBLIC', 'ETH0_IP'): + if k in context: + results['metadata']['local-hostname'] = context[k] + break + + # raw user data + if "USER_DATA" in context: + results['userdata'] = context["USER_DATA"] + elif "USERDATA" in context: + results['userdata'] = context["USERDATA"] + + # generate static /etc/network/interfaces + # only if there are any required context variables + # http://opennebula.org/documentation:rel3.8:cong#network_configuration + for k in context.keys(): + if re.match(r'^ETH\d+_IP$', k): + (out, _) = util.subp(['/sbin/ip', 'link']) + net = OpenNebulaNetwork(out, context) + results['network-interfaces'] = net.gen_conf() + break + + return results + + +# Used to match classes to dependencies +datasources = [ + (DataSourceOpenNebula, (sources.DEP_FILESYSTEM, )), + (DataSourceOpenNebulaNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), +] + + +# Return a list of data sources that match this set of dependencies +def get_datasource_list(depends): + return sources.list_from_depends(depends, datasources) diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py index 974c0407..7dc1fbde 100644 --- a/cloudinit/sources/__init__.py +++ b/cloudinit/sources/__init__.py @@ -53,9 +53,16 @@ class DataSource(object): self.userdata = None self.metadata = None self.userdata_raw = None + + # find the datasource config name. + # remove 'DataSource' from classname on front, and remove 'Net' on end. + # Both Foo and FooNet sources expect config in cfg['sources']['Foo'] name = type_utils.obj_name(self) if name.startswith(DS_PREFIX): name = name[len(DS_PREFIX):] + if name.endswith('Net'): + name = name[0:-3] + self.ds_cfg = util.get_cfg_by_path(self.sys_cfg, ("datasource", name), {}) if not ud_proc: @@ -144,7 +151,7 @@ class DataSource(object): return "iid-datasource" return str(self.metadata['instance-id']) - def get_hostname(self, fqdn=False): + def get_hostname(self, fqdn=False, resolve_ip=False): defdomain = "localdomain" defhost = "localhost" domain = defdomain @@ -168,7 +175,14 @@ class DataSource(object): # make up a hostname (LP: #475354) in format ip-xx.xx.xx.xx lhost = self.metadata['local-hostname'] if util.is_ipv4(lhost): - toks = ["ip-%s" % lhost.replace(".", "-")] + toks = [] + if resolve_ip: + toks = util.gethostbyaddr(lhost) + + if toks: + toks = str(toks).split('.') + else: + toks = ["ip-%s" % lhost.replace(".", "-")] else: toks = lhost.split(".") diff --git a/cloudinit/util.py b/cloudinit/util.py index 5032cc47..d50d3e18 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -955,6 +955,13 @@ def get_hostname(): return hostname +def gethostbyaddr(ip): + try: + return socket.gethostbyaddr(ip)[0] + except socket.herror: + return None + + def is_resolvable_url(url): """determine if this url is resolvable (existing or ip).""" return (is_resolvable(urlparse.urlparse(url).hostname)) diff --git a/doc/rtd/topics/datasources.rst b/doc/rtd/topics/datasources.rst index 59c58805..5543ed34 100644 --- a/doc/rtd/topics/datasources.rst +++ b/doc/rtd/topics/datasources.rst @@ -141,6 +141,12 @@ Config Drive .. include:: ../../sources/configdrive/README.rst --------------------------- +OpenNebula +--------------------------- + +.. include:: ../../sources/opennebula/README.rst + +--------------------------- Alt cloud --------------------------- diff --git a/doc/sources/opennebula/README.rst b/doc/sources/opennebula/README.rst new file mode 100644 index 00000000..4d7de27a --- /dev/null +++ b/doc/sources/opennebula/README.rst @@ -0,0 +1,142 @@ +The `OpenNebula`_ (ON) datasource supports the contextualization disk. + + See `contextualization overview`_, `contextualizing VMs`_ and + `network configuration`_ in the public documentation for + more information. + +OpenNebula's virtual machines are contextualized (parametrized) by +CD-ROM image, which contains a shell script *context.sh* with +custom variables defined on virtual machine start. There are no +fixed contextualization variables, but the datasource accepts +many used and recommended across the documentation. + +Datasource configuration +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Datasource accepts following configuration options. + +:: + + dsmode: + values: local, net, disabled + default: net + +Tells if this datasource will be processed in 'local' (pre-networking) or +'net' (post-networking) stage or even completely 'disabled'. + +:: + + parseuser: + default: nobody + +Unprivileged system user used for contextualization script +processing. + +Contextualization disk +~~~~~~~~~~~~~~~~~~~~~~ + +The following criteria are required: + +1. Must be formatted with `iso9660`_ filesystem + or have a *filesystem* label of **CONTEXT** or **CDROM** +2. Must contain file *context.sh* with contextualization variables. + File is generated by OpenNebula, it has a KEY='VALUE' format and + can be easily read by bash + +Contextualization variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are no fixed contextualization variables in OpenNebula, no standard. +Following variables were found on various places and revisions of +the OpenNebula documentation. Where multiple similar variables are +specified, only first found is taken. + +:: + + DSMODE + +Datasource mode configuration override. Values: local, net, disabled. + +:: + + DNS + ETH<x>_IP + ETH<x>_NETWORK + ETH<x>_MASK + ETH<x>_GATEWAY + ETH<x>_DOMAIN + ETH<x>_DNS + +Static `network configuration`_. + +:: + + HOSTNAME + +Instance hostname. + +:: + + PUBLIC_IP + IP_PUBLIC + ETH0_IP + +If no hostname has been specified, cloud-init will try to create hostname +from instance's IP address in 'local' dsmode. In 'net' dsmode, cloud-init +tries to resolve one of its IP addresses to get hostname. + +:: + + SSH_KEY + SSH_PUBLIC_KEY + +One or multiple SSH keys (separated by newlines) can be specified. + +:: + + USER_DATA + USERDATA + +cloud-init user data. + +Example configuration +~~~~~~~~~~~~~~~~~~~~~ + +This example cloud-init configuration (*cloud.cfg*) enables +OpenNebula datasource only in 'net' mode. + +:: + + disable_ec2_metadata: True + datasource_list: ['OpenNebula'] + datasource: + OpenNebula: + dsmode: net + parseuser: nobody + +Example VM's context section +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + CONTEXT=[ + PUBLIC_IP="$NIC[IP]", + SSH_KEY="$USER[SSH_KEY] + $USER[SSH_KEY1] + $USER[SSH_KEY2] ", + USER_DATA="#cloud-config + # see https://help.ubuntu.com/community/CloudInit + + packages: [] + + mounts: + - [vdc,none,swap,sw,0,0] + runcmd: + - echo 'Instance has been configured by cloud-init.' | wall + " ] + +.. _OpenNebula: http://opennebula.org/ +.. _contextualization overview: http://opennebula.org/documentation:documentation:context_overview +.. _contextualizing VMs: http://opennebula.org/documentation:documentation:cong +.. _network configuration: http://opennebula.org/documentation:documentation:cong#network_configuration +.. _iso9660: https://en.wikipedia.org/wiki/ISO_9660 diff --git a/tests/unittests/test_datasource/test_opennebula.py b/tests/unittests/test_datasource/test_opennebula.py new file mode 100644 index 00000000..45256a86 --- /dev/null +++ b/tests/unittests/test_datasource/test_opennebula.py @@ -0,0 +1,267 @@ +from cloudinit.sources import DataSourceOpenNebula as ds +from cloudinit import helpers +from cloudinit import util +from mocker import MockerTestCase +from tests.unittests.helpers import populate_dir + +import os +import pwd + +TEST_VARS = { + 'VAR1': 'single', + 'VAR2': 'double word', + 'VAR3': 'multi\nline\n', + 'VAR4': "'single'", + 'VAR5': "'double word'", + 'VAR6': "'multi\nline\n'", + 'VAR7': 'single\\t', + 'VAR8': 'double\\tword', + 'VAR9': 'multi\\t\nline\n', + 'VAR10': '\\', # expect \ + 'VAR11': '\'', # expect ' + 'VAR12': '$', # expect $ +} + +INVALID_CONTEXT = ';' +USER_DATA = '#cloud-config\napt_upgrade: true' +SSH_KEY = 'ssh-rsa AAAAB3NzaC1....sIkJhq8wdX+4I3A4cYbYP ubuntu@server-460-%i' +HOSTNAME = 'foo.example.com' +PUBLIC_IP = '10.0.0.3' + +CMD_IP_OUT = '''\ +1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 +2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000 + link/ether 02:00:0a:12:01:01 brd ff:ff:ff:ff:ff:ff +''' + + +class TestOpenNebulaDataSource(MockerTestCase): + parsed_user = None + + def setUp(self): + super(TestOpenNebulaDataSource, self).setUp() + self.tmp = self.makeDir() + self.paths = helpers.Paths({'cloud_dir': self.tmp}) + + # defaults for few tests + self.ds = ds.DataSourceOpenNebula + self.seed_dir = os.path.join(self.paths.seed_dir, "opennebula") + self.sys_cfg = {'datasource': {'OpenNebula': {'dsmode': 'local'}}} + + # we don't want 'sudo' called in tests. so we patch switch_user_cmd + def my_switch_user_cmd(user): + self.parsed_user = user + return [] + + self.switch_user_cmd_real = ds.switch_user_cmd + ds.switch_user_cmd = my_switch_user_cmd + + def tearDown(self): + ds.switch_user_cmd = self.switch_user_cmd_real + super(TestOpenNebulaDataSource, self).tearDown() + + def test_get_data_non_contextdisk(self): + orig_find_devs_with = util.find_devs_with + try: + # dont' try to lookup for CDs + util.find_devs_with = lambda n: [] + dsrc = self.ds(sys_cfg=self.sys_cfg, distro=None, paths=self.paths) + ret = dsrc.get_data() + self.assertFalse(ret) + finally: + util.find_devs_with = orig_find_devs_with + + def test_get_data_broken_contextdisk(self): + orig_find_devs_with = util.find_devs_with + try: + # dont' try to lookup for CDs + util.find_devs_with = lambda n: [] + populate_dir(self.seed_dir, {'context.sh': INVALID_CONTEXT}) + dsrc = self.ds(sys_cfg=self.sys_cfg, distro=None, paths=self.paths) + self.assertRaises(ds.BrokenContextDiskDir, dsrc.get_data) + finally: + util.find_devs_with = orig_find_devs_with + + def test_get_data_invalid_identity(self): + orig_find_devs_with = util.find_devs_with + try: + # generate non-existing system user name + sys_cfg = self.sys_cfg + invalid_user = 'invalid' + while not sys_cfg['datasource']['OpenNebula'].get('parseuser'): + try: + pwd.getpwnam(invalid_user) + invalid_user += 'X' + except KeyError: + sys_cfg['datasource']['OpenNebula']['parseuser'] = \ + invalid_user + + # dont' try to lookup for CDs + util.find_devs_with = lambda n: [] + populate_context_dir(self.seed_dir, {'KEY1': 'val1'}) + dsrc = self.ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) + self.assertRaises(ds.BrokenContextDiskDir, dsrc.get_data) + finally: + util.find_devs_with = orig_find_devs_with + + def test_get_data(self): + orig_find_devs_with = util.find_devs_with + try: + # dont' try to lookup for CDs + util.find_devs_with = lambda n: [] + populate_context_dir(self.seed_dir, {'KEY1': 'val1'}) + dsrc = self.ds(sys_cfg=self.sys_cfg, distro=None, paths=self.paths) + ret = dsrc.get_data() + self.assertTrue(ret) + finally: + util.find_devs_with = orig_find_devs_with + + def test_seed_dir_non_contextdisk(self): + self.assertRaises(ds.NonContextDiskDir, ds.read_context_disk_dir, + self.seed_dir) + + def test_seed_dir_empty1_context(self): + populate_dir(self.seed_dir, {'context.sh': ''}) + results = ds.read_context_disk_dir(self.seed_dir) + + self.assertEqual(results['userdata'], None) + self.assertEqual(results['metadata'], {}) + + def test_seed_dir_empty2_context(self): + populate_context_dir(self.seed_dir, {}) + results = ds.read_context_disk_dir(self.seed_dir) + + self.assertEqual(results['userdata'], None) + self.assertEqual(results['metadata'], {}) + + def test_seed_dir_broken_context(self): + populate_dir(self.seed_dir, {'context.sh': INVALID_CONTEXT}) + + self.assertRaises(ds.BrokenContextDiskDir, + ds.read_context_disk_dir, + self.seed_dir) + + def test_context_parser(self): + populate_context_dir(self.seed_dir, TEST_VARS) + results = ds.read_context_disk_dir(self.seed_dir) + + self.assertTrue('metadata' in results) + self.assertEqual(TEST_VARS, results['metadata']) + + def test_ssh_key(self): + public_keys = ['first key', 'second key'] + for c in range(4): + for k in ('SSH_KEY', 'SSH_PUBLIC_KEY'): + my_d = os.path.join(self.tmp, "%s-%i" % (k, c)) + populate_context_dir(my_d, {k: '\n'.join(public_keys)}) + results = ds.read_context_disk_dir(my_d) + + self.assertTrue('metadata' in results) + self.assertTrue('public-keys' in results['metadata']) + self.assertEqual(public_keys, + results['metadata']['public-keys']) + + public_keys.append(SSH_KEY % (c + 1,)) + + def test_user_data(self): + for k in ('USER_DATA', 'USERDATA'): + my_d = os.path.join(self.tmp, k) + populate_context_dir(my_d, {k: USER_DATA}) + results = ds.read_context_disk_dir(my_d) + + self.assertTrue('userdata' in results) + self.assertEqual(USER_DATA, results['userdata']) + + def test_hostname(self): + for k in ('HOSTNAME', 'PUBLIC_IP', 'IP_PUBLIC', 'ETH0_IP'): + my_d = os.path.join(self.tmp, k) + populate_context_dir(my_d, {k: PUBLIC_IP}) + results = ds.read_context_disk_dir(my_d) + + self.assertTrue('metadata' in results) + self.assertTrue('local-hostname' in results['metadata']) + self.assertEqual(PUBLIC_IP, results['metadata']['local-hostname']) + + def test_network_interfaces(self): + populate_context_dir(self.seed_dir, {'ETH0_IP': '1.2.3.4'}) + results = ds.read_context_disk_dir(self.seed_dir) + + self.assertTrue('network-interfaces' in results) + + def test_find_candidates(self): + def my_devs_with(criteria): + return { + "LABEL=CONTEXT": ["/dev/sdb"], + "LABEL=CDROM": ["/dev/sr0"], + "TYPE=iso9660": ["/dev/vdb"], + }.get(criteria, []) + + orig_find_devs_with = util.find_devs_with + try: + util.find_devs_with = my_devs_with + self.assertEqual(["/dev/sdb", "/dev/sr0", "/dev/vdb"], + ds.find_candidate_devs()) + finally: + util.find_devs_with = orig_find_devs_with + + +class TestOpenNebulaNetwork(MockerTestCase): + + def setUp(self): + super(TestOpenNebulaNetwork, self).setUp() + + def test_lo(self): + net = ds.OpenNebulaNetwork('', {}) + self.assertEqual(net.gen_conf(), u'''\ +auto lo +iface lo inet loopback +''') + + def test_eth0(self): + net = ds.OpenNebulaNetwork(CMD_IP_OUT, {}) + self.assertEqual(net.gen_conf(), u'''\ +auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet static + address 10.18.1.1 + network 10.18.1.0 + netmask 255.255.255.0 +''') + + def test_eth0_override(self): + context = { + 'DNS': '1.2.3.8', + 'ETH0_IP': '1.2.3.4', + 'ETH0_NETWORK': '1.2.3.0', + 'ETH0_MASK': '255.255.0.0', + 'ETH0_GATEWAY': '1.2.3.5', + 'ETH0_DOMAIN': 'example.com', + 'ETH0_DNS': '1.2.3.6 1.2.3.7' + } + + net = ds.OpenNebulaNetwork(CMD_IP_OUT, context) + self.assertEqual(net.gen_conf(), u'''\ +auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet static + address 1.2.3.4 + network 1.2.3.0 + netmask 255.255.0.0 + gateway 1.2.3.5 + dns-search example.com + dns-nameservers 1.2.3.8 1.2.3.6 1.2.3.7 +''') + + +def populate_context_dir(path, variables): + data = "# Context variables generated by OpenNebula\n" + for (k, v) in variables.iteritems(): + data += ("%s='%s'\n" % (k.upper(), v.replace(r"'", r"'\''"))) + populate_dir(path, {'context.sh': data}) + +# vi: ts=4 expandtab |