From ceffb35a2ddfb5f309417aaa68e7ff70199690fa Mon Sep 17 00:00:00 2001 From: Ben Howard Date: Fri, 7 Feb 2014 13:49:03 +0200 Subject: Made new ovf-env.xml handling more robust. Test cases included --- tests/unittests/test_datasource/test_azure.py | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) (limited to 'tests/unittests') diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py index aad84206..aa541a18 100644 --- a/tests/unittests/test_datasource/test_azure.py +++ b/tests/unittests/test_datasource/test_azure.py @@ -119,6 +119,10 @@ class TestAzureDataSource(MockerTestCase): {'ovf-env.xml': data['ovfcontent']}) mod = DataSourceAzure + ddir = "%s/var/lib/waagent" % self.tmp + mod.BUILTIN_DS_CONFIG['data_dir'] = ddir + if not os.path.isdir(ddir): + os.makedirs(ddir) self.apply_patches([(mod, 'list_possible_azure_ds_devs', dsdevs)]) @@ -338,6 +342,40 @@ class TestAzureDataSource(MockerTestCase): self.assertEqual(userdata, dsrc.userdata_raw) + def test_existing_ovf_same(self): + mydata = "FOOBAR" + odata = {'UserData': base64.b64encode(mydata)} + data = {'ovfcontent': construct_valid_ovf_env(data=odata)} + + with open("%s/ovf-env.xml" % self.tmp, 'w') as fp: + fp.write(construct_valid_ovf_env(data=odata)) + with open("%s/sem" % self.tmp, 'w') as fp: + fp.write("test") + + dsrc = self._get_ds(data) + ret = dsrc.get_data() + self.assertTrue(ret) + self.assertEqual(dsrc.userdata_raw, mydata) + self.assertTrue(os.path.exists("%s/sem" % self.tmp)) + + def test_existing_ovf_diff(self): + mydata = "FOOBAR" + odata = {'UserData': base64.b64encode(mydata)} + data = {'ovfcontent': construct_valid_ovf_env(data=odata)} + + data_dir = "%s/var/lib/waagent" % self.tmp + os.makedirs(data_dir) + with open("%s/ovf-env.xml" % data_dir, 'w') as fp: + fp.write(construct_valid_ovf_env()) + with open("%s/sem" % data_dir, 'w') as fp: + fp.write("test") + + dsrc = self._get_ds(data) + ret = dsrc.get_data() + self.assertTrue(ret) + self.assertEqual(dsrc.userdata_raw, mydata) + self.assertFalse(os.path.exists("%s/sem" % data_dir)) + class TestReadAzureOvf(MockerTestCase): def test_invalid_xml_raises_non_azure_ds(self): -- cgit v1.2.3 From c09e8cd6e81e478dd5276275418b70d5d946f479 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Mon, 10 Feb 2014 15:05:41 -0500 Subject: change behavior to only delete SharedConfig.xml. --- cloudinit/sources/DataSourceAzure.py | 33 ++++++---- tests/unittests/test_datasource/test_azure.py | 93 +++++++++++++++++---------- 2 files changed, 79 insertions(+), 47 deletions(-) (limited to 'tests/unittests') diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py index eee36d22..6b48a340 100644 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -84,14 +84,9 @@ class DataSourceAzureNet(sources.DataSource): candidates = [self.seed_dir] candidates.extend(list_possible_azure_ds_devs()) - previous_ovf_cfg = None if ddir: candidates.append(ddir) - previous_ovf_cfg = None - if os.path.exists("%s/ovf-env.xml" % ddir): - previous_ovf_cfg = load_azure_ds_dir(ddir) - found = None for cdev in candidates: @@ -109,11 +104,6 @@ class DataSourceAzureNet(sources.DataSource): LOG.warn("%s was not mountable" % cdev) continue - if ret != previous_ovf_cfg: - LOG.info(("instance configuration has changed, " - "removing old agent directory")) - util.del_dir(ddir) - (md, self.userdata_raw, cfg, files) = ret self.seed = cdev self.metadata = util.mergemanydict([md, DEFAULT_METADATA]) @@ -138,10 +128,27 @@ class DataSourceAzureNet(sources.DataSource): user_ds_cfg = util.get_cfg_by_path(self.cfg, DS_CFG_PATH, {}) self.ds_cfg = util.mergemanydict([user_ds_cfg, self.ds_cfg]) mycfg = self.ds_cfg + ddir = mycfg['data_dir'] + + if found != ddir: + cached_ovfenv = util.load_file( + os.path.join(ddir, 'ovf-env.xml'), quiet=True) + if cached_ovfenv != files['ovf-env.xml']: + # source was not walinux-agent's datadir, so we have to clean + # up so 'wait_for_files' doesn't return early due to stale data + toclean = ['SharedConfig.xml'] + cleaned = [] + for f in [os.path.join(ddir, f) for f in toclean]: + if os.path.exists(f): + util.del_file(f) + cleaned.append(f) + if cleaned: + LOG.info("removed stale file(s) in '%s': %s", + ddir, str(cleaned)) # walinux agent writes files world readable, but expects # the directory to be protected. - write_files(mycfg['data_dir'], files, dirmode=0700) + write_files(ddir, files, dirmode=0700) # handle the hostname 'publishing' try: @@ -159,13 +166,13 @@ class DataSourceAzureNet(sources.DataSource): util.logexc(LOG, "agent command '%s' failed.", mycfg['agent_command']) - shcfgxml = os.path.join(mycfg['data_dir'], "SharedConfig.xml") + shcfgxml = os.path.join(ddir, "SharedConfig.xml") wait_for = [shcfgxml] fp_files = [] for pk in self.cfg.get('_pubkeys', []): bname = str(pk['fingerprint'] + ".crt") - fp_files += [os.path.join(mycfg['data_dir'], bname)] + fp_files += [os.path.join(ddir, bname)] missing = util.log_time(logfunc=LOG.debug, msg="waiting for files", func=wait_for_files, diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py index aa541a18..44c537f4 100644 --- a/tests/unittests/test_datasource/test_azure.py +++ b/tests/unittests/test_datasource/test_azure.py @@ -1,4 +1,5 @@ from cloudinit import helpers +from cloudinit.util import load_file from cloudinit.sources import DataSourceAzure from tests.unittests.helpers import populate_dir @@ -6,6 +7,7 @@ import base64 import crypt from mocker import MockerTestCase import os +import stat import yaml @@ -72,6 +74,7 @@ class TestAzureDataSource(MockerTestCase): # patch cloud_dir, so our 'seed_dir' is guaranteed empty self.paths = helpers.Paths({'cloud_dir': self.tmp}) + self.waagent_d = os.path.join(self.tmp, 'var', 'lib', 'waagent') self.unapply = [] super(TestAzureDataSource, self).setUp() @@ -92,13 +95,6 @@ class TestAzureDataSource(MockerTestCase): def _invoke_agent(cmd): data['agent_invoked'] = cmd - def _write_files(datadir, files, dirmode): - data['files'] = {} - data['datadir'] = datadir - data['datadir_mode'] = dirmode - for (fname, content) in files.items(): - data['files'][fname] = content - def _wait_for_files(flist, _maxwait=None, _naplen=None): data['waited'] = flist return [] @@ -119,15 +115,11 @@ class TestAzureDataSource(MockerTestCase): {'ovf-env.xml': data['ovfcontent']}) mod = DataSourceAzure - ddir = "%s/var/lib/waagent" % self.tmp - mod.BUILTIN_DS_CONFIG['data_dir'] = ddir - if not os.path.isdir(ddir): - os.makedirs(ddir) + mod.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d self.apply_patches([(mod, 'list_possible_azure_ds_devs', dsdevs)]) self.apply_patches([(mod, 'invoke_agent', _invoke_agent), - (mod, 'write_files', _write_files), (mod, 'wait_for_files', _wait_for_files), (mod, 'pubkeys_from_crt_files', _pubkeys_from_crt_files), @@ -151,10 +143,18 @@ class TestAzureDataSource(MockerTestCase): self.assertTrue(ret) self.assertEqual(dsrc.userdata_raw, "") self.assertEqual(dsrc.metadata['local-hostname'], odata['HostName']) - self.assertTrue('ovf-env.xml' in data['files']) - self.assertEqual(0700, data['datadir_mode']) + self.assertTrue(os.path.isfile( + os.path.join(self.waagent_d, 'ovf-env.xml'))) self.assertEqual(dsrc.metadata['instance-id'], 'i-my-azure-id') + def test_waagent_d_has_0700_perms(self): + # we expect /var/lib/waagent to be created 0700 + dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) + ret = dsrc.get_data() + self.assertTrue(ret) + self.assertTrue(os.path.isdir(self.waagent_d)) + self.assertEqual(stat.S_IMODE(os.stat(self.waagent_d).st_mode), 0700) + def test_user_cfg_set_agent_command_plain(self): # set dscfg in via plaintext # we must have friendly-to-xml formatted plaintext in yaml_cfg @@ -342,39 +342,64 @@ class TestAzureDataSource(MockerTestCase): self.assertEqual(userdata, dsrc.userdata_raw) + def test_ovf_env_arrives_in_waagent_dir(self): + xml = construct_valid_ovf_env(data={}, userdata="FOODATA") + dsrc = self._get_ds({'ovfcontent': xml}) + dsrc.get_data() + + # 'data_dir' is '/var/lib/waagent' (walinux-agent's state dir) + # we expect that the ovf-env.xml file is copied there. + ovf_env_path = os.path.join(self.waagent_d, 'ovf-env.xml') + self.assertTrue(os.path.exists(ovf_env_path)) + self.assertEqual(xml, load_file(ovf_env_path)) + def test_existing_ovf_same(self): - mydata = "FOOBAR" - odata = {'UserData': base64.b64encode(mydata)} + # waagent/SharedConfig left alone if found ovf-env.xml same as cached + odata = {'UserData': base64.b64encode("SOMEUSERDATA")} data = {'ovfcontent': construct_valid_ovf_env(data=odata)} - with open("%s/ovf-env.xml" % self.tmp, 'w') as fp: - fp.write(construct_valid_ovf_env(data=odata)) - with open("%s/sem" % self.tmp, 'w') as fp: - fp.write("test") + populate_dir(self.waagent_d, + {'ovf-env.xml': data['ovfcontent'], + 'otherfile': 'otherfile-content', + 'SharedConfig.xml': 'mysharedconfig'}) dsrc = self._get_ds(data) ret = dsrc.get_data() self.assertTrue(ret) - self.assertEqual(dsrc.userdata_raw, mydata) - self.assertTrue(os.path.exists("%s/sem" % self.tmp)) + self.assertTrue(os.path.exists( + os.path.join(self.waagent_d, 'ovf-env.xml'))) + self.assertTrue(os.path.exists( + os.path.join(self.waagent_d, 'otherfile'))) + self.assertTrue(os.path.exists( + os.path.join(self.waagent_d, 'SharedConfig.xml'))) def test_existing_ovf_diff(self): - mydata = "FOOBAR" - odata = {'UserData': base64.b64encode(mydata)} - data = {'ovfcontent': construct_valid_ovf_env(data=odata)} + # waagent/SharedConfig must be removed if ovfenv is found elsewhere - data_dir = "%s/var/lib/waagent" % self.tmp - os.makedirs(data_dir) - with open("%s/ovf-env.xml" % data_dir, 'w') as fp: - fp.write(construct_valid_ovf_env()) - with open("%s/sem" % data_dir, 'w') as fp: - fp.write("test") + # 'get_data' should remove SharedConfig.xml in /var/lib/waagent + # if ovf-env.xml differs. + cached_ovfenv = construct_valid_ovf_env( + {'userdata': base64.b64encode("FOO_USERDATA")}) + new_ovfenv = construct_valid_ovf_env( + {'userdata': base64.b64encode("NEW_USERDATA")}) - dsrc = self._get_ds(data) + populate_dir(self.waagent_d, + {'ovf-env.xml': cached_ovfenv, + 'SharedConfig.xml': "mysharedconfigxml", + 'otherfile': 'otherfilecontent'}) + + dsrc = self._get_ds({'ovfcontent': new_ovfenv}) ret = dsrc.get_data() self.assertTrue(ret) - self.assertEqual(dsrc.userdata_raw, mydata) - self.assertFalse(os.path.exists("%s/sem" % data_dir)) + self.assertEqual(dsrc.userdata_raw, "NEW_USERDATA") + self.assertTrue(os.path.exists( + os.path.join(self.waagent_d, 'otherfile'))) + self.assertFalse( + os.path.exists(os.path.join(self.waagent_d, 'SharedConfig.xml'))) + self.assertTrue( + os.path.exists(os.path.join(self.waagent_d, 'ovf-env.xml'))) + self.assertEqual(new_ovfenv, + load_file(os.path.join(self.waagent_d, 'ovf-env.xml'))) class TestReadAzureOvf(MockerTestCase): -- cgit v1.2.3 From 09f392693efeacc7ac17cdab51c7299e928e394d Mon Sep 17 00:00:00 2001 From: Kiril Vladimiroff Date: Wed, 12 Feb 2014 12:14:49 +0200 Subject: Add CloudSigma data source --- cloudinit/cs_utils.py | 99 ++++++++++++++++++++++ cloudinit/settings.py | 1 + cloudinit/sources/DataSourceCloudSigma.py | 91 ++++++++++++++++++++ doc/sources/cloudsigma/README.rst | 34 ++++++++ requirements.txt | 4 +- tests/unittests/test_cs_util.py | 65 ++++++++++++++ tests/unittests/test_datasource/test_cloudsigma.py | 59 +++++++++++++ 7 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 cloudinit/cs_utils.py create mode 100644 cloudinit/sources/DataSourceCloudSigma.py create mode 100644 doc/sources/cloudsigma/README.rst create mode 100644 tests/unittests/test_cs_util.py create mode 100644 tests/unittests/test_datasource/test_cloudsigma.py (limited to 'tests/unittests') diff --git a/cloudinit/cs_utils.py b/cloudinit/cs_utils.py new file mode 100644 index 00000000..4e53c31a --- /dev/null +++ b/cloudinit/cs_utils.py @@ -0,0 +1,99 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2014 CloudSigma +# +# Author: Kiril Vladimiroff +# +# 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 . +""" +cepko implements easy-to-use communication with CloudSigma's VMs through +a virtual serial port without bothering with formatting the messages +properly nor parsing the output with the specific and sometimes +confusing shell tools for that purpose. + +Having the server definition accessible by the VM can ve useful in various +ways. For example it is possible to easily determine from within the VM, +which network interfaces are connected to public and which to private network. +Another use is to pass some data to initial VM setup scripts, like setting the +hostname to the VM name or passing ssh public keys through server meta. + +For more information take a look at the Server Context section of CloudSigma +API Docs: http://cloudsigma-docs.readthedocs.org/en/latest/server_context.html +""" +import json +import platform + +import serial + +SERIAL_PORT = '/dev/ttyS1' +if platform.system() == 'Windows': + SERIAL_PORT = 'COM2' + + +class Cepko(object): + """ + One instance of that object could be use for one or more + queries to the serial port. + """ + request_pattern = "<\n{}\n>" + + def get(self, key="", request_pattern=None): + if request_pattern is None: + request_pattern = self.request_pattern + return CepkoResult(request_pattern.format(key)) + + def all(self): + return self.get() + + def meta(self, key=""): + request_pattern = self.request_pattern.format("/meta/{}") + return self.get(key, request_pattern) + + def global_context(self, key=""): + request_pattern = self.request_pattern.format("/global_context/{}") + return self.get(key, request_pattern) + + +class CepkoResult(object): + """ + CepkoResult executes the request to the virtual serial port as soon + as the instance is initialized and stores the result in both raw and + marshalled format. + """ + def __init__(self, request): + self.request = request + self.raw_result = self._execute() + self.result = self._marshal(self.raw_result) + + def _execute(self): + connection = serial.Serial(SERIAL_PORT) + connection.write(self.request) + return connection.readline().strip('\x04\n') + + def _marshal(self, raw_result): + try: + return json.loads(raw_result) + except ValueError: + return raw_result + + def __len__(self): + return self.result.__len__() + + def __getitem__(self, key): + return self.result.__getitem__(key) + + def __contains__(self, item): + return self.result.__contains__(item) + + def __iter__(self): + return self.result.__iter__() diff --git a/cloudinit/settings.py b/cloudinit/settings.py index 7be2199a..7b0b18e7 100644 --- a/cloudinit/settings.py +++ b/cloudinit/settings.py @@ -37,6 +37,7 @@ CFG_BUILTIN = { 'OVF', 'MAAS', 'Ec2', + 'CloudSigma', 'CloudStack', 'SmartOS', # At the end to act as a 'catch' when none of the above work... diff --git a/cloudinit/sources/DataSourceCloudSigma.py b/cloudinit/sources/DataSourceCloudSigma.py new file mode 100644 index 00000000..78acd8a4 --- /dev/null +++ b/cloudinit/sources/DataSourceCloudSigma.py @@ -0,0 +1,91 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2014 CloudSigma +# +# Author: Kiril Vladimiroff +# +# 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 . +import re + +from cloudinit import log as logging +from cloudinit import sources +from cloudinit import util +from cloudinit.cs_utils import Cepko + +LOG = logging.getLogger(__name__) + +VALID_DSMODES = ("local", "net", "disabled") + + +class DataSourceCloudSigma(sources.DataSource): + """ + Uses cepko in order to gather the server context from the VM. + + For more information about CloudSigma's Server Context: + http://cloudsigma-docs.readthedocs.org/en/latest/server_context.html + """ + def __init__(self, sys_cfg, distro, paths): + self.dsmode = 'local' + self.cepko = Cepko() + self.ssh_public_key = '' + sources.DataSource.__init__(self, sys_cfg, distro, paths) + + def get_data(self): + """ + Metadata is the whole server context and /meta/cloud-config is used + as userdata. + """ + try: + server_context = self.cepko.all().result + server_meta = server_context['meta'] + self.userdata_raw = server_meta.get('cloudinit-user-data', "") + self.metadata = server_context + self.ssh_public_key = server_meta['ssh_public_key'] + + if server_meta.get('cloudinit-dsmode') in VALID_DSMODES: + self.dsmode = server_meta['cloudinit-dsmode'] + except: + util.logexc(LOG, "Failed reading from the serial port") + return False + return True + + def get_hostname(self, fqdn=False, resolve_ip=False): + """ + Cleans up and uses the server's name if the latter is set. Otherwise + the first part from uuid is being used. + """ + if re.match(r'^[A-Za-z0-9 -_\.]+$', self.metadata['name']): + return self.metadata['name'][:61] + else: + return self.metadata['uuid'].split('-')[0] + + def get_public_ssh_keys(self): + return [self.ssh_public_key] + + def get_instance_id(self): + return self.metadata['uuid'] + + +# Used to match classes to dependencies. Since this datasource uses the serial +# port network is not really required, so it's okay to load without it, too. +datasources = [ + (DataSourceCloudSigma, (sources.DEP_FILESYSTEM)), + (DataSourceCloudSigma, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), +] + + +def get_datasource_list(depends): + """ + Return a list of data sources that match this set of dependencies + """ + return sources.list_from_depends(depends, datasources) diff --git a/doc/sources/cloudsigma/README.rst b/doc/sources/cloudsigma/README.rst new file mode 100644 index 00000000..8cb2b0fe --- /dev/null +++ b/doc/sources/cloudsigma/README.rst @@ -0,0 +1,34 @@ +===================== +CloudSigma Datasource +===================== + +This datasource finds metadata and user-data from the `CloudSigma`_ cloud platform. +Data transfer occurs through a virtual serial port of the `CloudSigma`_'s VM and the +presence of network adapter is **NOT** a requirement, + + See `server context`_ in the public documentation for more information. + + +Setting a hostname +~~~~~~~~~~~~~~~~~~ + +By default the name of the server will be applied as a hostname on the first boot. + + +Providing user-data +~~~~~~~~~~~~~~~~~~~ + +You can provide user-data to the VM using the dedicated `meta field`_ in the `server context`_ +``cloudinit-user-data``. By default *cloud-config* format is expected there and the ``#cloud-config`` +header could be omitted. However since this is a raw-text field you could provide any of the valid +`config formats`_. + +If your user-data needs an internet connection you have to create a `meta field`_ in the `server context`_ +``cloudinit-dsmode`` and set "net" as value. If this field does not exist the default value is "local". + + + +.. _CloudSigma: http://cloudsigma.com/ +.. _server context: http://cloudsigma-docs.readthedocs.org/en/latest/server_context.html +.. _meta field: http://cloudsigma-docs.readthedocs.org/en/latest/meta.html +.. _config formats: http://cloudinit.readthedocs.org/en/latest/topics/format.html diff --git a/requirements.txt b/requirements.txt index 8f695c68..fdcbd143 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,8 +10,8 @@ PrettyTable # datasource is removed, this is no longer needed oauth -# This one is currently used only by the SmartOS datasource. If that -# datasource is removed, this is no longer needed +# This one is currently used only by the CloudSigma and SmartOS datasources. +# If these datasources are removed, this is no longer needed pyserial # This is only needed for places where we need to support configs in a manner diff --git a/tests/unittests/test_cs_util.py b/tests/unittests/test_cs_util.py new file mode 100644 index 00000000..7d59222b --- /dev/null +++ b/tests/unittests/test_cs_util.py @@ -0,0 +1,65 @@ +from mocker import MockerTestCase + +from cloudinit.cs_utils import Cepko + + +SERVER_CONTEXT = { + "cpu": 1000, + "cpus_instead_of_cores": False, + "global_context": {"some_global_key": "some_global_val"}, + "mem": 1073741824, + "meta": {"ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2E.../hQ5D5 john@doe"}, + "name": "test_server", + "requirements": [], + "smp": 1, + "tags": ["much server", "very performance"], + "uuid": "65b2fb23-8c03-4187-a3ba-8b7c919e889", + "vnc_password": "9e84d6cb49e46379" +} + + +class CepkoMock(Cepko): + def all(self): + return SERVER_CONTEXT + + def get(self, key="", request_pattern=None): + return SERVER_CONTEXT['tags'] + + +class CepkoResultTests(MockerTestCase): + def setUp(self): + self.mocked = self.mocker.replace("cloudinit.cs_utils.Cepko", + spec=CepkoMock, + count=False, + passthrough=False) + self.mocked() + self.mocker.result(CepkoMock()) + self.mocker.replay() + self.c = Cepko() + + def test_getitem(self): + result = self.c.all() + self.assertEqual("65b2fb23-8c03-4187-a3ba-8b7c919e889", result['uuid']) + self.assertEqual([], result['requirements']) + self.assertEqual("much server", result['tags'][0]) + self.assertEqual(1, result['smp']) + + def test_len(self): + self.assertEqual(len(SERVER_CONTEXT), len(self.c.all())) + + def test_contains(self): + result = self.c.all() + self.assertTrue('uuid' in result) + self.assertFalse('uid' in result) + self.assertTrue('meta' in result) + self.assertFalse('ssh_public_key' in result) + + def test_iter(self): + self.assertEqual(sorted(SERVER_CONTEXT.keys()), + sorted([key for key in self.c.all()])) + + def test_with_list_as_result(self): + result = self.c.get('tags') + self.assertEqual('much server', result[0]) + self.assertTrue('very performance' in result) + self.assertEqual(2, len(result)) diff --git a/tests/unittests/test_datasource/test_cloudsigma.py b/tests/unittests/test_datasource/test_cloudsigma.py new file mode 100644 index 00000000..3245aba1 --- /dev/null +++ b/tests/unittests/test_datasource/test_cloudsigma.py @@ -0,0 +1,59 @@ +# coding: utf-8 +from unittest import TestCase + +from cloudinit.cs_utils import Cepko +from cloudinit.sources import DataSourceCloudSigma + + +SERVER_CONTEXT = { + "cpu": 1000, + "cpus_instead_of_cores": False, + "global_context": {"some_global_key": "some_global_val"}, + "mem": 1073741824, + "meta": { + "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2E.../hQ5D5 john@doe", + "cloudinit-user-data": "#cloud-config\n\n...", + }, + "name": "test_server", + "requirements": [], + "smp": 1, + "tags": ["much server", "very performance"], + "uuid": "65b2fb23-8c03-4187-a3ba-8b7c919e8890", + "vnc_password": "9e84d6cb49e46379" +} + + +class CepkoMock(Cepko): + result = SERVER_CONTEXT + + def all(self): + return self + + +class DataSourceCloudSigmaTest(TestCase): + def setUp(self): + self.datasource = DataSourceCloudSigma.DataSourceCloudSigma("", "", "") + self.datasource.cepko = CepkoMock() + self.datasource.get_data() + + def test_get_hostname(self): + self.assertEqual("test_server", self.datasource.get_hostname()) + self.datasource.metadata['name'] = '' + self.assertEqual("65b2fb23", self.datasource.get_hostname()) + self.datasource.metadata['name'] = u'ั‚ะตัั‚' + self.assertEqual("65b2fb23", self.datasource.get_hostname()) + + def test_get_public_ssh_keys(self): + self.assertEqual([SERVER_CONTEXT['meta']['ssh_public_key']], + self.datasource.get_public_ssh_keys()) + + def test_get_instance_id(self): + self.assertEqual(SERVER_CONTEXT['uuid'], + self.datasource.get_instance_id()) + + def test_metadata(self): + self.assertEqual(self.datasource.metadata, SERVER_CONTEXT) + + def test_user_data(self): + self.assertEqual(self.datasource.userdata_raw, + SERVER_CONTEXT['meta']['cloudinit-user-data']) -- cgit v1.2.3