From 0d8a4f07d43659fee6dae7c65da8964dbf24519a Mon Sep 17 00:00:00 2001 From: Alex Kavanagh Date: Tue, 17 May 2016 20:54:56 +0000 Subject: [PATCH] Remove code from layer-openstack The code has shifted to the charm.openstack (at the time of writing) module that will be included in the built charm, rather than being in the layer. The idea is to make it easier to test/stub out openstack charms. --- .testr.conf | 8 - Makefile | 11 - README.md | 7 + layer.yaml | 6 +- lib/charm/__init__.py | 0 lib/charm/openstack/__init__.py | 0 lib/charm/openstack/adapters.py | 199 -------------- lib/charm/openstack/charm.py | 176 ------------ lib/charm/openstack/ip.py | 77 ------ test-requirements.txt | 7 - tox.ini | 25 -- unit_tests/__init__.py | 8 - unit_tests/test_charm_openstack_adapters.py | 198 -------------- unit_tests/test_charm_openstack_charm.py | 285 -------------------- unit_tests/test_charm_openstack_ip.py | 108 -------- unit_tests/utils.py | 48 ---- 16 files changed, 8 insertions(+), 1155 deletions(-) delete mode 100644 .testr.conf delete mode 100644 Makefile create mode 100644 README.md delete mode 100644 lib/charm/__init__.py delete mode 100644 lib/charm/openstack/__init__.py delete mode 100644 lib/charm/openstack/adapters.py delete mode 100644 lib/charm/openstack/charm.py delete mode 100644 lib/charm/openstack/ip.py delete mode 100644 test-requirements.txt delete mode 100644 tox.ini delete mode 100644 unit_tests/__init__.py delete mode 100644 unit_tests/test_charm_openstack_adapters.py delete mode 100644 unit_tests/test_charm_openstack_charm.py delete mode 100644 unit_tests/test_charm_openstack_ip.py delete mode 100644 unit_tests/utils.py diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index 801646b..0000000 --- a/.testr.conf +++ /dev/null @@ -1,8 +0,0 @@ -[DEFAULT] -test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ - OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ - OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ - ${PYTHON:-python} -m subunit.run discover -t ./ ./unit_tests $LISTOPT $IDOPTION - -test_id_option=--load-list $IDFILE -test_list_option=--list diff --git a/Makefile b/Makefile deleted file mode 100644 index 0f9b6d2..0000000 --- a/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/make -PYTHON := /usr/bin/env python - -clean: - @rm -rf .testrepository .unit-state.db .tox -lint: - @tox -e pep8 - -test: - @echo Starting unit tests... - @tox -e py27 diff --git a/README.md b/README.md new file mode 100644 index 0000000..3616109 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Where is the code? + +The code for this module is now held in the charm.openstack module which is +available at the following locations: + + - TODO PyPi + - TODO github.com/openstack-charms/charm.openstack diff --git a/layer.yaml b/layer.yaml index 03cb9f0..a590edd 100644 --- a/layer.yaml +++ b/layer.yaml @@ -1,7 +1,3 @@ includes: ['layer:basic'] ignore: - - 'unit_tests' - - 'Makefile' - - '.testr.conf' - - 'test-requirements.txt' - - 'tox.ini' + - 'README.md' diff --git a/lib/charm/__init__.py b/lib/charm/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lib/charm/openstack/__init__.py b/lib/charm/openstack/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lib/charm/openstack/adapters.py b/lib/charm/openstack/adapters.py deleted file mode 100644 index bcf0f7a..0000000 --- a/lib/charm/openstack/adapters.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Adapter classes and utilities for use with Reactive interfaces""" - -from charmhelpers.core import hookenv - - -class OpenStackRelationAdapter(object): - """ - Base adapter class for all OpenStack related adapters. - """ - - interface_type = None - """ - The generic type of the interface the adapter is wrapping. - """ - - def __init__(self, relation, accessors=None): - self.relation = relation - self.accessors = accessors or [] - self._setup_properties() - - @property - def relation_name(self): - """ - Name of the relation this adapter is handling. - """ - return self.relation.relation_name - - def _setup_properties(self): - """ - Setup property based accessors for an interfaces - auto accessors - - Note that the accessor is dynamic as each access calls the underlying - getattr() for each property access. - """ - self.accessors.extend(self.relation.auto_accessors) - for field in self.accessors: - meth_name = field.replace('-', '_') - # Get the relation property dynamically - # Note the additional lambda name: is to create a closure over - # meth_name so that a new 'name' gets created for each loop, - # otherwise the same variable meth_name is referenced in each of - # the internal lambdas. i.e. this is (lambda x: ...)(value) - setattr(self.__class__, - meth_name, - (lambda name: property( - lambda self: getattr( - self.relation, name)()))(meth_name)) - - -class RabbitMQRelationAdapter(OpenStackRelationAdapter): - """ - Adapter for the RabbitMQRequires relation interface. - """ - - interface_type = "messaging" - - def __init__(self, relation): - add_accessors = ['vhost', 'username'] - super(RabbitMQRelationAdapter, self).__init__(relation, add_accessors) - - @property - def host(self): - """ - Hostname that should be used to access RabbitMQ. - """ - if self.vip: - return self.vip - else: - return self.private_address - - @property - def hosts(self): - """ - Comma separated list of hosts that should be used - to access RabbitMQ. - """ - hosts = self.relation.rabbitmq_hosts() - if len(hosts) > 1: - return ','.join(hosts) - else: - return None - - -class DatabaseRelationAdapter(OpenStackRelationAdapter): - """ - Adapter for the Database relation interface. - """ - - interface_type = "database" - - def __init__(self, relation): - add_accessors = ['password', 'username', 'database'] - super(DatabaseRelationAdapter, self).__init__(relation, add_accessors) - - @property - def host(self): - """ - Hostname that should be used to access RabbitMQ. - """ - return self.relation.db_host() - - @property - def type(self): - return 'mysql' - - def get_uri(self, prefix=None): - if prefix: - uri = 'mysql://{}:{}@{}/{}'.format( - self.relation.username(prefix=prefix), - self.relation.password(prefix=prefix), - self.host, - self.relation.database(prefix=prefix), - ) - else: - uri = 'mysql://{}:{}@{}/{}'.format( - self.username, - self.password, - self.host, - self.database, - ) - try: - if self.ssl_ca: - uri = '{}?ssl_ca={}'.format(uri, self.ssl_ca) - if self.ssl_cert: - uri = ('{}&ssl_cert={}&ssl_key={}' - .format(uri, self.ssl_cert, self.ssl_key)) - except AttributeError: - # ignore ssl_ca or ssl_cert if not available - pass - return uri - - @property - def uri(self): - return self.get_uri() - - -class ConfigurationAdapter(object): - """ - Configuration Adapter which provides python based access - to all configuration options for the current charm. - """ - - def __init__(self): - _config = hookenv.config() - for k, v in _config.items(): - k = k.replace('-', '_') - setattr(self, k, v) - - -class OpenStackRelationAdapters(object): - """ - Base adapters class for OpenStack Charms, used to aggregate - the relations associated with a particular charm so that their - properties can be accessed using dot notation, e.g: - - adapters.amqp.private_address - """ - - relation_adapters = {} - """ - Dictionary mapping relation names to adapter classes, e.g: - - relation_adapters = { - 'amqp': RabbitMQRelationAdapter, - } - - By default, relations will be wrapped in an OpenStackRelationAdapter. - """ - - _adapters = { - 'amqp': RabbitMQRelationAdapter, - 'shared_db': DatabaseRelationAdapter, - } - """ - Default adapter mappings; may be overridden by relation adapters - in subclasses. - """ - - def __init__(self, relations, options=ConfigurationAdapter): - self._adapters.update(self.relation_adapters) - self._relations = [] - for relation in relations: - relation_name = relation.relation_name.replace('-', '_') - try: - relation_value = self._adapters[relation_name](relation) - except KeyError: - relation_value = OpenStackRelationAdapter(relation) - setattr(self, relation_name, relation_value) - self._relations.append(relation_name) - self.options = options() - self._relations.append('options') - - def __iter__(self): - """ - Iterate over the relations presented to the charm. - """ - for relation in self._relations: - yield relation, getattr(self, relation) diff --git a/lib/charm/openstack/charm.py b/lib/charm/openstack/charm.py deleted file mode 100644 index af2ab19..0000000 --- a/lib/charm/openstack/charm.py +++ /dev/null @@ -1,176 +0,0 @@ -"""Classes to support writing re-usable charms in the reactive framework""" - -from __future__ import absolute_import - -import subprocess -import os -from contextlib import contextmanager -from collections import OrderedDict - -from charmhelpers.contrib.openstack.utils import ( - configure_installation_source, -) -from charmhelpers.core.host import path_hash, service_restart -from charmhelpers.core.hookenv import config, status_set -from charmhelpers.fetch import ( - apt_install, - apt_update, - filter_installed_packages, -) -from charmhelpers.contrib.openstack.templating import get_loader -from charmhelpers.core.templating import render -from charmhelpers.core.hookenv import leader_get, leader_set -from charms.reactive.bus import set_state, remove_state - -from charm.openstack.ip import PUBLIC, INTERNAL, ADMIN, canonical_url - - -class OpenStackCharm(object): - """ - Base class for all OpenStack Charm classes; - encapulates general OpenStack charm payload operations - """ - - name = 'charmname' - - packages = [] - """Packages to install""" - - api_ports = {} - """ - Dictionary mapping services to ports for public, admin and - internal endpoints - """ - - service_type = None - """Keystone endpoint type""" - - default_service = None - """Default service for the charm""" - - restart_map = {} - sync_cmd = [] - services = [] - adapters_class = None - - def __init__(self, interfaces=None): - self.config = config() - # XXX It's not always liberty! - self.release = 'liberty' - if interfaces and self.adapters_class: - self.adapter_instance = self.adapters_class(interfaces) - - def install(self): - """ - Install packages related to this charm based on - contents of packages attribute. - """ - packages = filter_installed_packages(self.packages) - if packages: - status_set('maintenance', 'Installing packages') - apt_install(packages, fatal=True) - self.set_state('{}-installed'.format(self.name)) - - def set_state(self, state, value=None): - set_state(state, value) - - def remove_state(self, state): - remove_state(state) - - def api_port(self, service, endpoint_type=PUBLIC): - """ - Determine the API port for a particular endpoint type - """ - return self.api_ports[service][endpoint_type] - - def configure_source(self): - """Configure installation source""" - configure_installation_source(self.config['openstack-origin']) - apt_update(fatal=True) - - @property - def region(self): - """OpenStack Region""" - return self.config['region'] - - @property - def public_url(self): - """Public Endpoint URL""" - return "{}:{}".format(canonical_url(PUBLIC), - self.api_port(self.default_service, - PUBLIC)) - - @property - def admin_url(self): - """Admin Endpoint URL""" - return "{}:{}".format(canonical_url(ADMIN), - self.api_port(self.default_service, - ADMIN)) - - @property - def internal_url(self): - """Internal Endpoint URL""" - return "{}:{}".format(canonical_url(INTERNAL), - self.api_port(self.default_service, - INTERNAL)) - - @contextmanager - def restart_on_change(self): - checksums = {path: path_hash(path) for path in self.restart_map.keys()} - yield - restarts = [] - for path in self.restart_map: - if path_hash(path) != checksums[path]: - restarts += self.restart_map[path] - services_list = list(OrderedDict.fromkeys(restarts).keys()) - for service_name in services_list: - service_restart(service_name) - - def render_all_configs(self): - self.render_configs(self.restart_map.keys()) - - def render_configs(self, configs): - with self.restart_on_change(): - for conf in configs: - render(source=os.path.basename(conf), - template_loader=get_loader('templates/', self.release), - target=conf, - context=self.adapter_instance) - - def restart_all(self): - for svc in self.services: - service_restart(svc) - - def db_sync(self): - sync_done = leader_get(attribute='db-sync-done') - if not sync_done: - subprocess.check_call(self.sync_cmd) - leader_set({'db-sync-done': True}) - # Restart services immediatly after db sync as - # render_domain_config needs a working system - self.restart_all() - - -class OpenStackCharmFactory(object): - - releases = {} - """ - Dictionary mapping OpenStack releases to their associated - Charm class for this charm - """ - - first_release = "icehouse" - """ - First OpenStack release which this factory supports Charms for - """ - - @classmethod - def charm(cls, release=None, interfaces=None): - """ - Get an instance of the right charm for the - configured OpenStack series - """ - if release and release in cls.releases: - return cls.releases[release](interfaces=interfaces) - else: - return cls.releases[cls.first_release](interfaces=interfaces) diff --git a/lib/charm/openstack/ip.py b/lib/charm/openstack/ip.py deleted file mode 100644 index 32dadac..0000000 --- a/lib/charm/openstack/ip.py +++ /dev/null @@ -1,77 +0,0 @@ -from charmhelpers.core.hookenv import ( - config, - unit_get, -) - -from charmhelpers.contrib.network.ip import ( - get_address_in_network, - is_address_in_network, - is_ipv6, - get_ipv6_addr, -) - -from charmhelpers.contrib.hahelpers.cluster import is_clustered - -PUBLIC = 'public' -INTERNAL = 'int' -ADMIN = 'admin' - -_ADDRESS_MAP = { - PUBLIC: { - 'config': 'os-public-network', - 'fallback': 'public-address' - }, - INTERNAL: { - 'config': 'os-internal-network', - 'fallback': 'private-address' - }, - ADMIN: { - 'config': 'os-admin-network', - 'fallback': 'private-address' - } -} - - -def canonical_url(endpoint_type=PUBLIC): - ''' - Returns the correct HTTP URL to this host given the state of HTTPS - configuration, hacluster and charm configuration. - - :endpoint_type str: The endpoint type to resolve. - - :returns str: Base URL for services on the current service unit. - ''' - scheme = 'http' -# if 'https' in configs.complete_contexts(): -# scheme = 'https' - address = resolve_address(endpoint_type) - if is_ipv6(address): - address = "[{}]".format(address) - return "{0}://{1}".format(scheme, address) - - -def resolve_address(endpoint_type=PUBLIC): - resolved_address = None - if is_clustered(): - if config(_ADDRESS_MAP[endpoint_type]['config']) is None: - # Assume vip is simple and pass back directly - resolved_address = config('vip') - else: - for vip in config('vip').split(): - if is_address_in_network( - config(_ADDRESS_MAP[endpoint_type]['config']), - vip): - resolved_address = vip - else: - if config('prefer-ipv6'): - fallback_addr = get_ipv6_addr(exc_list=[config('vip')])[0] - else: - fallback_addr = unit_get(_ADDRESS_MAP[endpoint_type]['fallback']) - resolved_address = get_address_in_network( - config(_ADDRESS_MAP[endpoint_type]['config']), fallback_addr) - - if resolved_address is None: - raise ValueError('Unable to resolve a suitable IP address' - ' based on charm state and configuration') - else: - return resolved_address diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 3be6af7..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -flake8>=2.2.4,<=2.4.1 -os-testr>=0.4.1 -paramiko<2.0 -charm-tools>=2.0.0 -charms.reactive -mock>=1.2 -coverage>=3.6 diff --git a/tox.ini b/tox.ini deleted file mode 100644 index ff49815..0000000 --- a/tox.ini +++ /dev/null @@ -1,25 +0,0 @@ -[tox] -envlist = pep8,py27 -skipsdist = True - -[testenv] -setenv = VIRTUAL_ENV={envdir} - PYTHONHASHSEED=0 -install_command = - pip install --allow-unverified python-apt {opts} {packages} -commands = ostestr {posargs} - -[testenv:py27] -basepython = python2.7 -deps = -r{toxinidir}/test-requirements.txt - -[testenv:pep8] -basepython = python2.7 -deps = -r{toxinidir}/test-requirements.txt -commands = flake8 {posargs} lib unit_tests - -[testenv:venv] -commands = {posargs} - -[flake8] -ignore = E402,E226 diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py deleted file mode 100644 index 7f41985..0000000 --- a/unit_tests/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys -import mock - - -sys.path.append('./lib') -# mock out some charmhelpers libraries as they have apt install side effects -sys.modules['charmhelpers.contrib.openstack.utils'] = mock.MagicMock() -sys.modules['charmhelpers.contrib.network.ip'] = mock.MagicMock() diff --git a/unit_tests/test_charm_openstack_adapters.py b/unit_tests/test_charm_openstack_adapters.py deleted file mode 100644 index ab249e5..0000000 --- a/unit_tests/test_charm_openstack_adapters.py +++ /dev/null @@ -1,198 +0,0 @@ -# Note that the unit_tests/__init__.py has the following lines to stop -# side effects from the imorts from charm helpers. - -# sys.path.append('./lib') -# mock out some charmhelpers libraries as they have apt install side effects -# sys.modules['charmhelpers.contrib.openstack.utils'] = mock.MagicMock() -# sys.modules['charmhelpers.contrib.network.ip'] = mock.MagicMock() - -import unittest -import mock - -import charm.openstack.adapters as adapters - - -class MyRelation(object): - - auto_accessors = ['this', 'that'] - relation_name = 'my-name' - - def this(self): - return 'this' - - def that(self): - return 'that' - - def some(self): - return 'thing' - - -class TestOpenStackRelationAdapter(unittest.TestCase): - - def test_class(self): - ad = adapters.OpenStackRelationAdapter(MyRelation(), ['some']) - self.assertEqual(ad.this, 'this') - self.assertEqual(ad.that, 'that') - self.assertEqual(ad.some, 'thing') - self.assertEqual(ad.relation_name, 'my-name') - with self.assertRaises(AttributeError): - ad.relation_name = 'hello' - - -class FakeRabbitMQRelation(): - - auto_accessors = ['vip', 'private_address'] - relation_name = 'amqp' - - def __init__(self, vip=None): - self._vip = vip - - def vip(self): - return self._vip - - def private_address(self): - return 'private-address' - - def rabbitmq_hosts(self): - return ['host1', 'host2'] - - def vhost(self): - return 'vhost' - - def username(self): - return 'fakename' - - -class TestRabbitMQRelationAdapter(unittest.TestCase): - - def test_class(self): - fake = FakeRabbitMQRelation(None) - mq = adapters.RabbitMQRelationAdapter(fake) - self.assertEqual(mq.vhost, 'vhost') - self.assertEqual(mq.username, 'fakename') - self.assertEqual(mq.host, 'private-address') - # TODO: can't do the following 2 lines as not dynamic accessors - # fake._vip = 'vip1' - # self.assertEqual(mq.host, 'vip1') - self.assertEqual(mq.hosts, 'host1,host2') - - -class FakeDatabaseRelation(): - - auto_accessors = [] - relation_name = 'shared_db' - - def db_host(self): - return 'host1' - - def username(self, prefix=''): - return 'username1{}'.format(prefix) - - def password(self, prefix=''): - return 'password1{}'.format(prefix) - - def database(self, prefix=''): - return 'database1{}'.format(prefix) - - -class SSLDatabaseRelationAdapter(adapters.DatabaseRelationAdapter): - - ssl_ca = 'my-ca' - ssl_cert = 'my-cert' - ssl_key = 'my-key' - - -class TestDatabaseRelationAdapter(unittest.TestCase): - - def test_class(self): - fake = FakeDatabaseRelation() - db = adapters.DatabaseRelationAdapter(fake) - self.assertEqual(db.host, 'host1') - self.assertEqual(db.type, 'mysql') - self.assertEqual(db.password, 'password1') - self.assertEqual(db.username, 'username1') - self.assertEqual(db.database, 'database1') - self.assertEqual(db.uri, 'mysql://username1:password1@host1/database1') - self.assertEqual(db.get_uri('x'), - 'mysql://username1x:password1x@host1/database1x') - # test the ssl feature of the base class - db = SSLDatabaseRelationAdapter(fake) - self.assertEqual(db.uri, - 'mysql://username1:password1@host1/database1' - '?ssl_ca=my-ca' - '&ssl_cert=my-cert&ssl_key=my-key') - - -class TestConfigurationAdapter(unittest.TestCase): - - def test_class(self): - test_config = { - 'one': 1, - 'two': 2, - 'three': 3, - 'that-one': 4 - } - with mock.patch.object(adapters.hookenv, 'config', - new=lambda: test_config): - c = adapters.ConfigurationAdapter() - self.assertEqual(c.one, 1) - self.assertEqual(c.three, 3) - self.assertEqual(c.that_one, 4) - - -class TestOpenStackRelationAdapters(unittest.TestCase): - # test the OpenStackRelationAdapters() class, and then derive from it to - # test the additonal relation_adapters member on __init__ - - def test_class(self): - test_config = { - 'one': 1, - 'two': 2, - 'three': 3, - 'that-one': 4 - } - with mock.patch.object(adapters.hookenv, 'config', - new=lambda: test_config): - amqp = FakeRabbitMQRelation() - shared_db = FakeDatabaseRelation() - mine = MyRelation() - a = adapters.OpenStackRelationAdapters([amqp, shared_db, mine]) - self.assertEqual(a.amqp.private_address, 'private-address') - self.assertEqual(a.my_name.this, 'this') - items = list(a) - self.assertEqual(items[0][0], 'amqp') - self.assertEqual(items[1][0], 'shared_db') - self.assertEqual(items[2][0], 'my_name') - self.assertEqual(items[3][0], 'options') - - -class MyRelationAdapter(adapters.OpenStackRelationAdapter): - - @property - def us(self): - return self.this + '-us' - - -class MyOpenStackRelationAdapters(adapters.OpenStackRelationAdapters): - - relation_adapters = { - 'my_name': MyRelationAdapter, - } - - -class TestCustomOpenStackRelationAdapters(unittest.TestCase): - - def test_class(self): - test_config = { - 'one': 1, - 'two': 2, - 'three': 3, - 'that-one': 4 - } - with mock.patch.object(adapters.hookenv, 'config', - new=lambda: test_config): - amqp = FakeRabbitMQRelation() - shared_db = FakeDatabaseRelation() - mine = MyRelation() - a = MyOpenStackRelationAdapters([amqp, shared_db, mine]) - self.assertEqual(a.my_name.us, 'this-us') diff --git a/unit_tests/test_charm_openstack_charm.py b/unit_tests/test_charm_openstack_charm.py deleted file mode 100644 index 898f51a..0000000 --- a/unit_tests/test_charm_openstack_charm.py +++ /dev/null @@ -1,285 +0,0 @@ -# Note that the unit_tests/__init__.py has the following lines to stop -# side effects from the imorts from charm helpers. - -# sys.path.append('./lib') -# mock out some charmhelpers libraries as they have apt install side effects -# sys.modules['charmhelpers.contrib.openstack.utils'] = mock.MagicMock() -# sys.modules['charmhelpers.contrib.network.ip'] = mock.MagicMock() - -import mock - -import utils - -import charm.openstack.charm as chm - -TEST_CONFIG = {'config': True} - - -class BaseOpenStackCharmTest(utils.BaseTestCase): - - @classmethod - def setUpClass(cls): - cls.patched_config = mock.patch.object(chm, 'config') - cls.patched_config_started = cls.patched_config.start() - - @classmethod - def tearDownClass(cls): - cls.patched_config.stop() - cls.patched_config_started = None - cls.patched_config = None - - def setUp(self, target_cls, test_config): - super(BaseOpenStackCharmTest, self).setUp() - # set up the return value on the mock before instantiating the class to - # get the config into the class.config. - chm.config.return_value = test_config - self.target = target_cls() - - def tearDown(self): - self.target = None - super(BaseOpenStackCharmTest, self).tearDown() - - def patch_target(self, attr, return_value=None, name=None, new=None): - # uses BaseTestCase.patch_object() to patch targer. - self.patch_object(self.target, attr, return_value, name, new) - - -class TestOpenStackCharm(BaseOpenStackCharmTest): - # Note that this only tests the OpenStackCharm() class, which has not very - # useful defaults for testing. In order to test all the code without too - # many mocks, a separate test dervied charm class is used below. - - def setUp(self): - super(TestOpenStackCharm, self).setUp(chm.OpenStackCharm, TEST_CONFIG) - - def test__init__(self): - # Note cls.setUpClass() creates an OpenStackCharm() instance - self.assertEqual(chm.config(), TEST_CONFIG) - self.assertEqual(self.target.config, TEST_CONFIG) - self.assertEqual(self.target.release, 'liberty') - - def test_install(self): - # only tests that the default set_state is called - self.patch_target('set_state') - self.patch_object(chm, 'filter_installed_packages', - name='fip', - return_value=None) - self.target.install() - self.target.set_state.assert_called_once_with('charmname-installed') - self.fip.assert_called_once_with([]) - - def test_set_state(self): - # tests that OpenStackCharm.set_state() calls set_state() global - self.patch_object(chm, 'set_state') - self.target.set_state('hello') - self.set_state.assert_called_once_with('hello', None) - self.set_state.reset_mock() - self.target.set_state('hello', 'there') - self.set_state.assert_called_once_with('hello', 'there') - - def test_remove_state(self): - # tests that OpenStackCharm.remove_state() calls remove_state() global - self.patch_object(chm, 'remove_state') - self.target.remove_state('hello') - self.remove_state.assert_called_once_with('hello') - - def test_configure_source(self): - self.patch_object(chm, 'configure_installation_source', name='cis') - self.patch_object(chm, 'apt_update') - self.patch_target('config', new={'openstack-origin': 'an-origin'}) - self.target.configure_source() - self.cis.assert_called_once_with('an-origin') - self.apt_update.assert_called_once_with(fatal=True) - - def test_region(self): - self.patch_target('config', new={'region': 'a-region'}) - self.assertEqual(self.target.region, 'a-region') - - def test_restart_on_change(self): - from collections import OrderedDict - hashs = OrderedDict([ - ('path1', 100), - ('path2', 200), - ('path3', 300), - ('path4', 400), - ]) - self.target.restart_map = { - 'path1': ['s1'], - 'path2': ['s2'], - 'path3': ['s3'], - 'path4': ['s2', 's4'], - } - self.patch_object(chm, 'path_hash') - self.path_hash.side_effect = lambda x: hashs[x] - self.patch_object(chm, 'service_restart') - # slightly awkard, in that we need to test a context manager - with self.target.restart_on_change(): - # test with no restarts - pass - self.assertEqual(self.service_restart.call_count, 0) - - with self.target.restart_on_change(): - # test with path1 and path3 restarts - for k in ['path1', 'path3']: - hashs[k] += 1 - self.assertEqual(self.service_restart.call_count, 2) - self.service_restart.assert_any_call('s1') - self.service_restart.assert_any_call('s3') - - # test with path2 and path4 and that s2 only gets restarted once - self.service_restart.reset_mock() - with self.target.restart_on_change(): - for k in ['path2', 'path4']: - hashs[k] += 1 - self.assertEqual(self.service_restart.call_count, 2) - calls = [mock.call('s2'), mock.call('s4')] - self.service_restart.assert_has_calls(calls) - - def test_restart_all(self): - self.patch_object(chm, 'service_restart') - self.patch_target('services', new=['s1', 's2']) - self.target.restart_all() - self.assertEqual(self.service_restart.call_args_list, - [mock.call('s1'), mock.call('s2')]) - - def test_db_sync(self): - self.patch_object(chm, 'leader_get') - self.patch_object(chm, 'leader_set') - self.patch_object(chm, 'subprocess', name='subprocess') - self.patch_target('restart_all') - # first check with leader_get returning True - self.leader_get.return_value = True - self.target.db_sync() - self.leader_get.assert_called_once_with(attribute='db-sync-done') - self.subprocess.check_call.assert_not_called() - self.leader_set.assert_not_called() - self.restart_all.assert_not_called() - # Now check with leader_get returning False - self.leader_get.reset_mock() - self.leader_get.return_value = False - self.target.sync_cmd = ['a', 'cmd'] - self.target.db_sync() - self.leader_get.assert_called_once_with(attribute='db-sync-done') - self.subprocess.check_call.assert_called_once_with(['a', 'cmd']) - self.leader_set.assert_called_once_with({'db-sync-done': True}) - self.restart_all.assert_called_once_with() - - -class MyAdapter(object): - - def __init__(self, interfaces): - self.interfaces = interfaces - - -class MyOpenStackCharm(chm.OpenStackCharm): - - name = 'my-charm' - packages = ['p1', 'p2', 'p3', 'package-to-filter'] - api_ports = { - 'service1': { - chm.PUBLIC: 1, - chm.INTERNAL: 2, - }, - 'service2': { - chm.PUBLIC: 3, - }, - 'my-default-service': { - chm.PUBLIC: 1234, - chm.ADMIN: 2468, - chm.INTERNAL: 3579, - }, - } - service_type = 'my-service-type' - default_service = 'my-default-service' - restart_map = { - 'path1': ['s1'], - 'path2': ['s2'], - 'path3': ['s3'], - 'path4': ['s2', 's4'], - } - sync_cmd = ['my-sync-cmd', 'param1'] - services = ['my-default-service', 'my-second-service'] - adapters_class = MyAdapter - - -class TestMyOpenStackCharm(BaseOpenStackCharmTest): - - def setUp(self): - def make_open_stack_charm(): - return MyOpenStackCharm(['interface1', 'interface2']) - - super(TestMyOpenStackCharm, self).setUp(make_open_stack_charm, - TEST_CONFIG) - - def test_install(self): - # tests that the packages are filtered before installation - self.patch_target('set_state') - self.patch_object(chm, 'filter_installed_packages', - return_value=None, - name='fip') - self.fip.side_effect = lambda x: ['p1', 'p2'] - self.patch_object(chm, 'status_set') - self.patch_object(chm, 'apt_install') - self.target.install() - self.target.set_state.assert_called_once_with('my-charm-installed') - self.fip.assert_called_once_with(self.target.packages) - self.status_set.assert_called_once_with('maintenance', - 'Installing packages') - - def test_api_port(self): - self.assertEqual(self.target.api_port('service1'), 1) - self.assertEqual(self.target.api_port('service1', chm.PUBLIC), 1) - self.assertEqual(self.target.api_port('service2'), 3) - with self.assertRaises(KeyError): - self.target.api_port('service3') - with self.assertRaises(KeyError): - self.target.api_port('service2', chm.INTERNAL) - - def test_public_url(self): - self.patch_object(chm, 'canonical_url', return_value='my-ip-address') - self.assertEqual(self.target.public_url, 'my-ip-address:1234') - self.canonical_url.assert_called_once_with(chm.PUBLIC) - - def test_admin_url(self): - self.patch_object(chm, 'canonical_url', return_value='my-ip-address') - self.assertEqual(self.target.admin_url, 'my-ip-address:2468') - self.canonical_url.assert_called_once_with(chm.ADMIN) - - def test_internal_url(self): - self.patch_object(chm, 'canonical_url', return_value='my-ip-address') - self.assertEqual(self.target.internal_url, 'my-ip-address:3579') - self.canonical_url.assert_called_once_with(chm.INTERNAL) - - def test_render_all_configs(self): - self.patch_target('render_configs') - self.target.render_all_configs() - self.assertEqual(self.render_configs.call_count, 1) - args = self.render_configs.call_args_list[0][0][0] - self.assertEqual(['path1', 'path2', 'path3', 'path4'], - sorted(args)) - - def test_render_configs(self): - # give us a way to check that the context manager was called. - from contextlib import contextmanager - d = [0] - - @contextmanager - def fake_restart_on_change(): - d[0] += 1 - yield - - self.patch_target('restart_on_change', new=fake_restart_on_change) - self.patch_object(chm, 'render') - self.patch_object(chm, 'get_loader', return_value='my-loader') - # self.patch_target('adapter_instance', new='my-adapter') - self.target.render_configs(['path1']) - self.assertEqual(d[0], 1) - self.render.assert_called_once_with( - source='path1', - template_loader='my-loader', - target='path1', - context=mock.ANY) - # assert the context was an MyAdapter instance. - context = self.render.call_args_list[0][1]['context'] - assert isinstance(context, MyAdapter) - self.assertEqual(context.interfaces, ['interface1', 'interface2']) diff --git a/unit_tests/test_charm_openstack_ip.py b/unit_tests/test_charm_openstack_ip.py deleted file mode 100644 index 3845c04..0000000 --- a/unit_tests/test_charm_openstack_ip.py +++ /dev/null @@ -1,108 +0,0 @@ -# Note that the unit_tests/__init__.py has the following lines to stop -# side effects from the imorts from charm helpers. - -# sys.path.append('./lib') -# mock out some charmhelpers libraries as they have apt install side effects -# sys.modules['charmhelpers.contrib.openstack.utils'] = mock.MagicMock() -# sys.modules['charmhelpers.contrib.network.ip'] = mock.MagicMock() - -import utils - -import charm.openstack.ip as ip - - -class TestCharmOpenStackIp(utils.BaseTestCase): - - def test_canonical_url(self): - self.patch_object(ip, 'resolve_address', return_value='address1') - self.patch_object(ip, 'is_ipv6', return_value=False) - # not ipv6 - url = ip.canonical_url() - self.assertEqual(url, 'http://address1') - self.resolve_address.assert_called_once_with(ip.PUBLIC) - # is ipv6 - self.is_ipv6.return_value = True - self.resolve_address.reset_mock() - url = ip.canonical_url() - self.assertEqual(url, 'http://[address1]') - self.resolve_address.assert_called_once_with(ip.PUBLIC) - # test we check for enpoint type - self.is_ipv6.return_value = False - self.resolve_address.reset_mock() - url = ip.canonical_url(ip.INTERNAL) - self.resolve_address.assert_called_once_with(ip.INTERNAL) - - def test_resolve_address(self): - self.patch_object(ip, 'is_clustered') - self.patch_object(ip, 'config') - self.patch_object(ip, 'is_address_in_network') - self.patch_object(ip, 'get_ipv6_addr') - self.patch_object(ip, 'unit_get') - self.patch_object(ip, 'get_address_in_network') - - # define a fake_config() that returns predictable results and remembers - # what it was called with. - calls_list = [] - _config = { - 'vip': 'vip-address', - 'prefer-ipv6': False, - 'os-public-network': 'the-public-network', - 'os-internal-network': 'the-internal-network', - 'os-admin-network': 'the-admin-network', - } - - def fake_config(*args): - calls_list.append(args) - return _config[args[0]] - - self.config.side_effect = fake_config - - # first test, if not clustered, that the function uses unit_get() and - # get_address_in_network to get a real address. - # for the default PUBLIC endpoint - self.is_clustered.return_value = False - self.get_address_in_network.return_value = 'got-address' - self.unit_get.return_value = 'unit-get-address' - addr = ip.resolve_address() - self.assertEqual(addr, 'got-address') - self.assertEqual(calls_list, - [('prefer-ipv6',), ('os-public-network',)]) - self.unit_get.assert_called_once_with('public-address') - self.get_address_in_network.assert_called_once_with( - 'the-public-network', 'unit-get-address') - - # second test: not clusted, prefer-ipv6 is True - _config['prefer-ipv6'] = True - calls_list = [] - self.get_ipv6_addr.return_value = ['ipv6-addr'] - self.get_address_in_network.reset_mock() - addr = ip.resolve_address() - self.get_ipv6_addr.assert_called_once_with(exc_list=['vip-address']) - self.get_address_in_network.assert_called_once_with( - 'the-public-network', 'ipv6-addr') - - # Third test: clustered, and config(...) returns None - self.is_clustered.return_value = True - _config['os-public-network'] = None - calls_list = [] - addr = ip.resolve_address() - self.assertEqual(calls_list, [('os-public-network',), ('vip',)]) - - # Fourth test: clustered, and config(...) returns not None - _config['os-public-network'] = 'the-public-network' - calls_list = [] - _config['vip'] = 'vip1 vip2' - self.is_address_in_network.return_value = (False, True) - addr = ip.resolve_address() - self.assertEqual(calls_list, [ - ('os-public-network',), - ('vip',), - ('os-public-network',), - ('os-public-network',)]) - self.assertEqual(addr, 'vip2') - - # Finally resolved_address returns None -> ValueError() - # allow vip to not be found: - self.is_address_in_network.return_value = False - with self.assertRaises(ValueError): - addr = ip.resolve_address() diff --git a/unit_tests/utils.py b/unit_tests/utils.py deleted file mode 100644 index bce68f5..0000000 --- a/unit_tests/utils.py +++ /dev/null @@ -1,48 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Note that the unit_tests/__init__.py also mocks out two charmhelpers imports -# that have side effects that try to apt install modules: -# sys.modules['charmhelpers.contrib.openstack.utils'] = mock.MagicMock() -# sys.modules['charmhelpers.contrib.network.ip'] = mock.MagicMock() - - -import unittest -import mock - - -class BaseTestCase(unittest.TestCase): - - def setUp(self): - self._patches = {} - self._patches_start = {} - - def tearDown(self): - for k, v in self._patches.items(): - v.stop() - setattr(self, k, None) - self._patches = None - self._patches_start = None - - def patch_object(self, obj, attr, return_value=None, name=None, new=None): - if name is None: - name = attr - if new is not None: - mocked = mock.patch.object(obj, attr, new=new) - else: - mocked = mock.patch.object(obj, attr) - self._patches[name] = mocked - started = mocked.start() - if new is None: - started.return_value = return_value - self._patches_start[name] = started - setattr(self, name, started)