diff --git a/hooks/quantum_contexts.py b/hooks/quantum_contexts.py index 6dfe5bd7..61745888 100644 --- a/hooks/quantum_contexts.py +++ b/hooks/quantum_contexts.py @@ -1,15 +1,39 @@ # vim: set ts=4:et +import os +import uuid +import socket from charmhelpers.core.hookenv import ( config, relation_ids, related_units, relation_get, + unit_get, + cached, +) +from charmhelpers.core.host import ( + apt_install, ) from charmhelpers.contrib.openstack.context import ( OSContextGenerator, context_complete ) -import quantum_utils as qutils + +DB_USER = "quantum" +QUANTUM_DB = "quantum" +NOVA_DB_USER = "nova" +NOVA_DB = "nova" + +OVS = "ovs" +NVP = "nvp" + +OVS_PLUGIN = \ + "quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2" +NVP_PLUGIN = \ + "quantum.plugins.nicira.nicira_nvp_plugin.QuantumPlugin.NvpPluginV2" +CORE_PLUGIN = { + OVS: OVS_PLUGIN, + NVP: NVP_PLUGIN +} class NetworkServiceContext(OSContextGenerator): @@ -58,9 +82,9 @@ class ExternalPortContext(OSContextGenerator): class QuantumGatewayContext(OSContextGenerator): def __call__(self): ctxt = { - 'shared_secret': qutils.get_shared_secret(), - 'local_ip': qutils.get_host_ip(), - 'core_plugin': qutils.CORE_PLUGIN[config('plugin')], + 'shared_secret': get_shared_secret(), + 'local_ip': get_host_ip(), + 'core_plugin': CORE_PLUGIN[config('plugin')], 'plugin': config('plugin') } return ctxt @@ -75,15 +99,49 @@ class QuantumSharedDBContext(OSContextGenerator): ctxt = { 'database_host': relation_get('db_host', rid=rid, unit=unit), - 'quantum_database': qutils.QUANTUM_DB, - 'quantum_user': qutils.DB_USER, + 'quantum_database': QUANTUM_DB, + 'quantum_user': DB_USER, 'quantum_password': relation_get('quantum_password', rid=rid, unit=unit), - 'nova_database': qutils.NOVA_DB, - 'nova_user': qutils.NOVA_DB_USER, + 'nova_database': NOVA_DB, + 'nova_user': NOVA_DB_USER, 'nova_password': relation_get('nova_password', rid=rid, unit=unit) } + print ctxt if context_complete(ctxt): return ctxt return {} + + +@cached +def get_host_ip(hostname=None): + try: + import dns.resolver + except ImportError: + apt_install('python-dnspython', fatal=True) + import dns.resolver + hostname = hostname or unit_get('private-address') + try: + # Test to see if already an IPv4 address + socket.inet_aton(hostname) + return hostname + except socket.error: + answers = dns.resolver.query(hostname, 'A') + if answers: + return answers[0].address + + +SHARED_SECRET = "/etc/quantum/secret.txt" + + +def get_shared_secret(): + secret = None + if not os.path.exists(SHARED_SECRET): + secret = str(uuid.uuid4()) + with open(SHARED_SECRET, 'w') as secret_file: + secret_file.write(secret) + else: + with open(SHARED_SECRET, 'r') as secret_file: + secret = secret_file.read().strip() + return secret diff --git a/hooks/quantum_hooks.py b/hooks/quantum_hooks.py index 98defe20..d4e10d5e 100755 --- a/hooks/quantum_hooks.py +++ b/hooks/quantum_hooks.py @@ -33,11 +33,13 @@ from quantum_utils import ( get_packages, get_early_packages, valid_plugin, - DB_USER, QUANTUM_DB, - NOVA_DB_USER, NOVA_DB, configure_ovs, reassign_agent_resources, ) +from quantum_contexts import ( + DB_USER, QUANTUM_DB, + NOVA_DB_USER, NOVA_DB, +) hooks = Hooks() CONFIGS = register_configs() diff --git a/hooks/quantum_utils.py b/hooks/quantum_utils.py index 99cfadac..3ff52e86 100644 --- a/hooks/quantum_utils.py +++ b/hooks/quantum_utils.py @@ -1,11 +1,6 @@ -import os -import uuid -import socket from charmhelpers.core.hookenv import ( log, config, - unit_get, - cached ) from charmhelpers.core.host import ( apt_install, @@ -22,23 +17,19 @@ from charmhelpers.contrib.openstack.utils import ( ) import charmhelpers.contrib.openstack.context as context import charmhelpers.contrib.openstack.templating as templating -import quantum_contexts +from quantum_contexts import ( + CORE_PLUGIN, + OVS, NVP, + QuantumGatewayContext, + NetworkServiceContext, + QuantumSharedDBContext, + ExternalPortContext, +) from collections import OrderedDict -OVS = "ovs" -NVP = "nvp" - -OVS_PLUGIN = \ - "quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2" -NVP_PLUGIN = \ - "quantum.plugins.nicira.nicira_nvp_plugin.QuantumPlugin.NvpPluginV2" -CORE_PLUGIN = { - OVS: OVS_PLUGIN, - NVP: NVP_PLUGIN -} - def valid_plugin(): + print config('plugin') return config('plugin') in CORE_PLUGIN OVS_PLUGIN_CONF = \ @@ -94,18 +85,18 @@ NOVA_CONF = "/etc/nova/nova.conf" SHARED_CONFIG_FILES = { DHCP_AGENT_CONF: { - 'hook_contexts': [quantum_contexts.QuantumGatewayContext()], + 'hook_contexts': [QuantumGatewayContext()], 'services': ['quantum-dhcp-agent'] }, METADATA_AGENT_CONF: { - 'hook_contexts': [quantum_contexts.NetworkServiceContext()], + 'hook_contexts': [NetworkServiceContext()], 'services': ['quantum-metadata-agent'] }, NOVA_CONF: { 'hook_contexts': [context.AMQPContext(), - quantum_contexts.QuantumSharedDBContext(), - quantum_contexts.NetworkServiceContext(), - quantum_contexts.QuantumGatewayContext()], + QuantumSharedDBContext(), + NetworkServiceContext(), + QuantumGatewayContext()], 'services': ['nova-api-metadata'] }, } @@ -113,24 +104,24 @@ SHARED_CONFIG_FILES = { OVS_CONFIG_FILES = { QUANTUM_CONF: { 'hook_contexts': [context.AMQPContext(), - quantum_contexts.QuantumGatewayContext()], + QuantumGatewayContext()], 'services': ['quantum-l3-agent', 'quantum-dhcp-agent', 'quantum-metadata-agent', 'quantum-plugin-openvswitch-agent'] }, L3_AGENT_CONF: { - 'hook_contexts': [quantum_contexts.NetworkServiceContext()], + 'hook_contexts': [NetworkServiceContext()], 'services': ['quantum-l3-agent'] }, # TODO: Check to see if this is actually required OVS_PLUGIN_CONF: { - 'hook_contexts': [quantum_contexts.QuantumSharedDBContext(), - quantum_contexts.QuantumGatewayContext()], + 'hook_contexts': [QuantumSharedDBContext(), + QuantumGatewayContext()], 'services': ['quantum-plugin-openvswitch-agent'] }, EXT_PORT_CONF: { - 'hook_contexts': [quantum_contexts.ExternalPortContext()], + 'hook_contexts': [ExternalPortContext()], 'services': [] } } @@ -182,39 +173,16 @@ def restart_map(): return OrderedDict(_map) -DB_USER = "quantum" -QUANTUM_DB = "quantum" -KEYSTONE_SERVICE = "quantum" -NOVA_DB_USER = "nova" -NOVA_DB = "nova" - -RABBIT_USER = "nova" -RABBIT_VHOST = "nova" - INT_BRIDGE = "br-int" EXT_BRIDGE = "br-ex" -SHARED_SECRET = "/etc/quantum/secret.txt" - - -def get_shared_secret(): - secret = None - if not os.path.exists(SHARED_SECRET): - secret = str(uuid.uuid4()) - with open(SHARED_SECRET, 'w') as secret_file: - secret_file.write(secret) - else: - with open(SHARED_SECRET, 'r') as secret_file: - secret = secret_file.read().strip() - return secret - DHCP_AGENT = "DHCP Agent" L3_AGENT = "L3 Agent" def reassign_agent_resources(): ''' Use agent scheduler API to detect down agents and re-schedule ''' - env = quantum_contexts.NetworkServiceContext()() + env = NetworkServiceContext()() if not env: log('Unable to re-assign resources at this time') return @@ -305,24 +273,6 @@ def do_openstack_upgrade(configs): configs.set_release(openstack_release=new_os_rel) -@cached -def get_host_ip(hostname=None): - try: - import dns.resolver - except ImportError: - apt_install('python-dnspython', fatal=True) - import dns.resolver - hostname = hostname or unit_get('private-address') - try: - # Test to see if already an IPv4 address - socket.inet_aton(hostname) - return hostname - except socket.error: - answers = dns.resolver.query(hostname, 'A') - if answers: - return answers[0].address - - def configure_ovs(): if config('plugin') == OVS: add_bridge(INT_BRIDGE) diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py index e69de29b..f80aab3d 100644 --- a/unit_tests/__init__.py +++ b/unit_tests/__init__.py @@ -0,0 +1,2 @@ +import sys +sys.path.append('hooks') diff --git a/unit_tests/test_quantum_contexts.py b/unit_tests/test_quantum_contexts.py new file mode 100644 index 00000000..edecfd7e --- /dev/null +++ b/unit_tests/test_quantum_contexts.py @@ -0,0 +1,218 @@ +from mock import MagicMock, patch +import quantum_contexts +from contextlib import contextmanager + +from test_utils import ( + CharmTestCase +) + +TO_PATCH = [ + 'config', + 'relation_get', + 'relation_ids', + 'related_units', + 'context_complete', + 'unit_get', + 'apt_install', +] + + +@contextmanager +def patch_open(): + '''Patch open() to allow mocking both open() itself and the file that is + yielded. + + Yields the mock for "open" and "file", respectively.''' + mock_open = MagicMock(spec=open) + mock_file = MagicMock(spec=file) + + @contextmanager + def stub_open(*args, **kwargs): + mock_open(*args, **kwargs) + yield mock_file + + with patch('__builtin__.open', stub_open): + yield mock_open, mock_file + + +class _TestQuantumContext(CharmTestCase): + def setUp(self): + super(_TestQuantumContext, self).setUp(quantum_contexts, TO_PATCH) + self.config.side_effect = self.test_config.get + + def test_not_related(self): + self.relation_ids.return_value = [] + self.assertEquals(self.context(), {}) + + def test_no_units(self): + self.relation_ids.return_value = [] + self.relation_ids.return_value = ['foo'] + self.related_units.return_value = [] + self.assertEquals(self.context(), {}) + + def test_no_data(self): + self.relation_ids.return_value = ['foo'] + self.related_units.return_value = ['bar'] + self.relation_get.side_effect = self.test_relation.get + self.context_complete.return_value = False + self.assertEquals(self.context(), {}) + + def test_data_multi_unit(self): + self.relation_ids.return_value = ['foo'] + self.related_units.return_value = ['bar', 'baz'] + self.context_complete.return_value = True + self.relation_get.side_effect = self.test_relation.get + self.assertEquals(self.context(), self.data_result) + + def test_data_single_unit(self): + self.relation_ids.return_value = ['foo'] + self.related_units.return_value = ['bar'] + self.context_complete.return_value = True + self.relation_get.side_effect = self.test_relation.get + self.assertEquals(self.context(), self.data_result) + + +class TestQuantumSharedDBContext(_TestQuantumContext): + def setUp(self): + super(TestQuantumSharedDBContext, self).setUp() + self.context = quantum_contexts.QuantumSharedDBContext() + self.test_relation.set( + {'db_host': '10.5.0.1', + 'nova_password': 'novapass', + 'quantum_password': 'quantumpass'} + ) + self.data_result = { + 'database_host': '10.5.0.1', + 'nova_user': 'nova', + 'nova_password': 'novapass', + 'nova_database': 'nova', + 'quantum_user': 'quantum', + 'quantum_password': 'quantumpass', + 'quantum_database': 'quantum' + } + + +class TestNetworkServiceContext(_TestQuantumContext): + def setUp(self): + super(TestNetworkServiceContext, self).setUp() + self.context = quantum_contexts.NetworkServiceContext() + self.test_relation.set( + {'keystone_host': '10.5.0.1', + 'service_port': '5000', + 'auth_port': '20000', + 'service_tenant': 'tenant', + 'service_username': 'username', + 'service_password': 'password', + 'quantum_host': '10.5.0.2', + 'quantum_port': '9696', + 'quantum_url': 'http://10.5.0.2:9696/v2', + 'region': 'aregion'} + ) + self.data_result = { + 'keystone_host': '10.5.0.1', + 'service_port': '5000', + 'auth_port': '20000', + 'service_tenant': 'tenant', + 'service_username': 'username', + 'service_password': 'password', + 'quantum_host': '10.5.0.2', + 'quantum_port': '9696', + 'quantum_url': 'http://10.5.0.2:9696/v2', + 'region': 'aregion', + 'service_protocol': 'http', + 'auth_protocol': 'http', + } + + +class TestExternalPortContext(CharmTestCase): + def setUp(self): + super(TestExternalPortContext, self).setUp(quantum_contexts, + TO_PATCH) + + def test_no_ext_port(self): + self.config.return_value = None + self.assertEquals(quantum_contexts.ExternalPortContext()(), + None) + + def test_ext_port(self): + self.config.return_value = 'eth1010' + self.assertEquals(quantum_contexts.ExternalPortContext()(), + {'ext_port': 'eth1010'}) + + +class TestQuantumGatewayContext(CharmTestCase): + def setUp(self): + super(TestQuantumGatewayContext, self).setUp(quantum_contexts, + TO_PATCH) + + @patch.object(quantum_contexts, 'get_shared_secret') + @patch.object(quantum_contexts, 'get_host_ip') + def test_all(self, _host_ip, _secret): + self.config.return_value = 'ovs' + _host_ip.return_value = '10.5.0.1' + _secret.return_value = 'testsecret' + self.assertEquals(quantum_contexts.QuantumGatewayContext()(), + {'shared_secret': 'testsecret', + 'local_ip': '10.5.0.1', + 'core_plugin': + "quantum.plugins.openvswitch.ovs_quantum_plugin." + "OVSQuantumPluginV2", + 'plugin': 'ovs'}) + + +class TestSharedSecret(CharmTestCase): + def setUp(self): + super(TestSharedSecret, self).setUp(quantum_contexts, + TO_PATCH) + + @patch('os.path') + @patch('uuid.uuid4') + def test_secret_created_stored(self, _uuid4, _path): + _path.exists.return_value = False + _uuid4.return_value = 'secret_thing' + with patch_open() as (_open, _file): + self.assertEquals(quantum_contexts.get_shared_secret(), + 'secret_thing') + _open.assert_called_with(quantum_contexts.SHARED_SECRET, 'w') + _file.write.assert_called_with('secret_thing') + + @patch('os.path') + def test_secret_retrieved(self, _path): + _path.exists.return_value = True + with patch_open() as (_open, _file): + _file.read.return_value = 'secret_thing\n' + self.assertEquals(quantum_contexts.get_shared_secret(), + 'secret_thing') + _open.assert_called_with(quantum_contexts.SHARED_SECRET, 'r') + + +class TestHostIP(CharmTestCase): + def setUp(self): + super(TestHostIP, self).setUp(quantum_contexts, + TO_PATCH) + + def test_get_host_ip_already_ip(self): + self.assertEquals(quantum_contexts.get_host_ip('10.5.0.1'), + '10.5.0.1') + + def test_get_host_ip_noarg(self): + self.unit_get.return_value = "10.5.0.1" + self.assertEquals(quantum_contexts.get_host_ip(), + '10.5.0.1') + + @patch('dns.resolver.query') + def test_get_host_ip_hostname_unresolvable(self, _query): + class NXDOMAIN(Exception): + pass + _query.side_effect = NXDOMAIN() + self.assertRaises(NXDOMAIN, quantum_contexts.get_host_ip, + 'missing.example.com') + + @patch('dns.resolver.query') + def test_get_host_ip_hostname_resolvable(self, _query): + data = MagicMock() + data.address = '10.5.0.1' + _query.return_value = [data] + self.assertEquals(quantum_contexts.get_host_ip('myhost.example.com'), + '10.5.0.1') + _query.assert_called_with('myhost.example.com', 'A') diff --git a/unit_tests/test_quantum_hooks.py b/unit_tests/test_quantum_hooks.py new file mode 100644 index 00000000..be81b916 --- /dev/null +++ b/unit_tests/test_quantum_hooks.py @@ -0,0 +1,143 @@ +from mock import MagicMock, patch, call +import quantum_utils as utils +_register_configs = utils.register_configs +_restart_map = utils.restart_map +utils.register_configs = MagicMock() +utils.restart_map = MagicMock() +import quantum_hooks as hooks +utils.register_configs = _register_configs +utils.restart_map = _restart_map + +from charmhelpers.contrib.hahelpers.cluster import HAIncompleteConfig +from test_utils import CharmTestCase + + +TO_PATCH = [ + 'config', + 'configure_installation_source', + 'valid_plugin', + 'apt_update', + 'apt_install', + 'filter_installed_packages', + 'get_early_packages', + 'get_packages', + 'log', + 'do_openstack_upgrade', + 'openstack_upgrade_available', + 'CONFIGS', + 'configure_ovs', + 'relation_set', + 'unit_get', + 'relation_get', + 'install_ca_cert', + 'eligible_leader', + 'reassign_agent_resources', +] + + +class TestQuantumHooks(CharmTestCase): + + def setUp(self): + super(TestQuantumHooks, self).setUp(hooks, TO_PATCH) + self.config.side_effect = self.test_config.get + self.test_config.set('openstack-origin', 'cloud:precise-havana') + self.test_config.set('plugin', 'ovs') + + def _call_hook(self, hookname): + hooks.hooks.execute([ + 'hooks/{}'.format(hookname)]) + + def test_install_hook(self): + self.valid_plugin.return_value = True + _pkgs = ['foo', 'bar'] + self.filter_installed_packages.return_value = _pkgs + self._call_hook('install') + self.configure_installation_source.assert_called_with( + 'cloud:precise-havana' + ) + self.apt_update.assert_called_with(fatal=True) + self.apt_install.assert_has_calls([ + call(_pkgs, fatal=True), + call(_pkgs, fatal=True), + ]) + self.get_early_packages.assert_called() + self.get_packages.assert_called() + + @patch('sys.exit') + def test_install_hook_invalid_plugin(self, _exit): + self.valid_plugin.return_value = False + self._call_hook('install') + self.log.assert_called() + _exit.assert_called_with(1) + + def test_config_changed_upgrade(self): + self.openstack_upgrade_available.return_value = True + self.valid_plugin.return_value = True + self._call_hook('config-changed') + self.do_openstack_upgrade.assert_called_with(self.CONFIGS) + self.CONFIGS.write_all.assert_called() + self.configure_ovs.assert_called() + + @patch('sys.exit') + def test_config_changed_invalid_plugin(self, _exit): + self.valid_plugin.return_value = False + self._call_hook('config-changed') + self.log.assert_called() + _exit.assert_called_with(1) + + def test_upgrade_charm(self): + _install = self.patch('install') + _config_changed = self.patch('config_changed') + self._call_hook('upgrade-charm') + _install.assert_called() + _config_changed.assert_called() + + def test_db_joined(self): + self.unit_get.return_value = 'myhostname' + self._call_hook('shared-db-relation-joined') + self.relation_set.assert_called_with( + quantum_username='quantum', + quantum_database='quantum', + quantum_hostname='myhostname', + nova_username='nova', + nova_database='nova', + nova_hostname='myhostname', + ) + + def test_amqp_joined(self): + self._call_hook('amqp-relation-joined') + self.relation_set.assert_called_with( + username='nova', + vhost='nova', + ) + + def test_amqp_changed(self): + self._call_hook('amqp-relation-changed') + self.CONFIGS.write_all.assert_called() + + def test_shared_db_changed(self): + self._call_hook('shared-db-relation-changed') + self.CONFIGS.write_all.assert_called() + + def test_nm_changed(self): + self.relation_get.return_value = "cert" + self._call_hook('quantum-network-service-relation-changed') + self.CONFIGS.write_all.assert_called() + self.install_ca_cert.assert_called_with('cert') + + def test_cluster_departed_nvp(self): + self.test_config.set('plugin', 'nvp') + self._call_hook('cluster-relation-departed') + self.log.assert_called() + self.eligible_leader.assert_not_called() + self.reassign_agent_resources.assert_not_called() + + def test_cluster_departed_ovs_not_leader(self): + self.eligible_leader.return_value = False + self._call_hook('cluster-relation-departed') + self.reassign_agent_resources.assert_not_called() + + def test_cluster_departed_ovs_leader(self): + self.eligible_leader.return_value = True + self._call_hook('cluster-relation-departed') + self.reassign_agent_resources.assert_called() diff --git a/unit_tests/test_quantum_utils.py b/unit_tests/test_quantum_utils.py new file mode 100644 index 00000000..f5a45adc --- /dev/null +++ b/unit_tests/test_quantum_utils.py @@ -0,0 +1,169 @@ +from mock import MagicMock, patch, call +import charmhelpers.contrib.openstack.templating as templating +templating.OSConfigRenderer = MagicMock() +import quantum_utils +from collections import OrderedDict + +from test_utils import ( + CharmTestCase +) + +TO_PATCH = [ + 'get_os_codename_package', + 'config', + 'get_os_codename_install_source', + 'apt_update', + 'apt_install', + 'configure_installation_source', + 'log', + 'add_bridge', + 'add_bridge_port' +] + + +class TestQuantumUtils(CharmTestCase): + def setUp(self): + super(TestQuantumUtils, self).setUp(quantum_utils, TO_PATCH) + + def test_valid_plugin(self): + self.config.return_value = 'ovs' + self.assertTrue(quantum_utils.valid_plugin()) + self.config.return_value = 'nvp' + self.assertTrue(quantum_utils.valid_plugin()) + + def test_invalid_plugin(self): + self.config.return_value = 'invalid' + self.assertFalse(quantum_utils.valid_plugin()) + + def test_get_early_packages_ovs(self): + self.config.return_value = 'ovs' + self.assertEquals(quantum_utils.get_early_packages(), + ['openvswitch-datapath-dkms']) + + def test_get_early_packages_nvp(self): + self.config.return_value = 'nvp' + self.assertEquals(quantum_utils.get_early_packages(), + []) + + def test_get_packages_ovs(self): + self.config.return_value = 'ovs' + self.assertNotEqual(quantum_utils.get_packages(), []) + + def test_configure_ovs_ovs_ext_port(self): + self.config.side_effect = self.test_config.get + self.test_config.set('plugin', 'ovs') + self.test_config.set('ext-port', 'eth0') + quantum_utils.configure_ovs() + self.add_bridge.assert_has_calls([ + call('br-int'), + call('br-ex') + ]) + self.add_bridge_port.assert_called_with('br-ex', 'eth0') + + def test_configure_ovs_nvp(self): + self.config.return_value = 'nvp' + quantum_utils.configure_ovs() + self.add_bridge.assert_called_with('br-int') + + def test_do_openstack_upgrade(self): + self.config.side_effect = self.test_config.get + self.test_config.set('openstack-origin', 'cloud:precise-havana') + self.test_config.set('plugin', 'ovs') + self.config.return_value = 'cloud:precise-havana' + self.get_os_codename_install_source.return_value = 'havana' + configs = MagicMock() + quantum_utils.do_openstack_upgrade(configs) + configs.set_release.assert_called_with(openstack_release='havana') + self.log.assert_called() + self.apt_update.assert_called_with(fatal=True) + dpkg_opts = [ + '--option', 'Dpkg::Options::=--force-confnew', + '--option', 'Dpkg::Options::=--force-confdef', + ] + self.apt_install.assert_called_with( + packages=quantum_utils.GATEWAY_PKGS['ovs'], + options=dpkg_opts, fatal=True + ) + self.configure_installation_source.assert_called_with( + 'cloud:precise-havana' + ) + + def test_register_configs_ovs(self): + self.config.return_value = 'ovs' + self.get_os_codename_package.return_value = 'havana' + configs = quantum_utils.register_configs() + confs = [quantum_utils.DHCP_AGENT_CONF, + quantum_utils.METADATA_AGENT_CONF, + quantum_utils.NOVA_CONF, + quantum_utils.QUANTUM_CONF, + quantum_utils.L3_AGENT_CONF, + quantum_utils.OVS_PLUGIN_CONF, + quantum_utils.EXT_PORT_CONF] + for conf in confs: + configs.register.assert_any_call( + conf, + quantum_utils.CONFIG_FILES[quantum_utils.OVS][conf] + ['hook_contexts'] + ) + + def test_restart_map_ovs(self): + self.config.return_value = 'ovs' + ex_map = OrderedDict([ + (quantum_utils.L3_AGENT_CONF, ['quantum-l3-agent']), + (quantum_utils.OVS_PLUGIN_CONF, + ['quantum-plugin-openvswitch-agent']), + (quantum_utils.NOVA_CONF, ['nova-api-metadata']), + (quantum_utils.METADATA_AGENT_CONF, ['quantum-metadata-agent']), + (quantum_utils.DHCP_AGENT_CONF, ['quantum-dhcp-agent']), + (quantum_utils.QUANTUM_CONF, ['quantum-l3-agent', + 'quantum-dhcp-agent', + 'quantum-metadata-agent', + 'quantum-plugin-openvswitch-agent']) + ]) + self.assertEquals(quantum_utils.restart_map(), ex_map) + + def test_register_configs_nvp(self): + self.config.return_value = 'nvp' + self.get_os_codename_package.return_value = 'havana' + configs = quantum_utils.register_configs() + confs = [quantum_utils.DHCP_AGENT_CONF, + quantum_utils.METADATA_AGENT_CONF, + quantum_utils.NOVA_CONF, + quantum_utils.QUANTUM_CONF] + for conf in confs: + configs.register.assert_any_call( + conf, + quantum_utils.CONFIG_FILES[quantum_utils.NVP][conf] + ['hook_contexts'] + ) + + def test_restart_map_nvp(self): + self.config.return_value = 'nvp' + ex_map = OrderedDict([ + (quantum_utils.DHCP_AGENT_CONF, ['quantum-dhcp-agent']), + (quantum_utils.NOVA_CONF, ['nova-api-metadata']), + (quantum_utils.QUANTUM_CONF, ['quantum-dhcp-agent', + 'quantum-metadata-agent']), + (quantum_utils.METADATA_AGENT_CONF, ['quantum-metadata-agent']), + ]) + self.assertEquals(quantum_utils.restart_map(), ex_map) + + def test_register_configs_pre_install(self): + self.config.return_value = 'ovs' + self.get_os_codename_package.return_value = None + configs = quantum_utils.register_configs() + confs = [quantum_utils.DHCP_AGENT_CONF, + quantum_utils.METADATA_AGENT_CONF, + quantum_utils.NOVA_CONF, + quantum_utils.QUANTUM_CONF, + quantum_utils.L3_AGENT_CONF, + quantum_utils.OVS_PLUGIN_CONF, + quantum_utils.EXT_PORT_CONF] + for conf in confs: + configs.register.assert_any_call( + conf, + quantum_utils.CONFIG_FILES[quantum_utils.OVS][conf] + ['hook_contexts'] + ) + + diff --git a/unit_tests/test_utils.py b/unit_tests/test_utils.py new file mode 100644 index 00000000..fd7fe233 --- /dev/null +++ b/unit_tests/test_utils.py @@ -0,0 +1,97 @@ +import logging +import unittest +import os +import yaml + +from mock import patch + + +def load_config(): + ''' + Walk backwords from __file__ looking for config.yaml, load and return the + 'options' section' + ''' + config = None + f = __file__ + while config is None: + d = os.path.dirname(f) + if os.path.isfile(os.path.join(d, 'config.yaml')): + config = os.path.join(d, 'config.yaml') + break + f = d + + if not config: + logging.error('Could not find config.yaml in any parent directory ' + 'of %s. ' % file) + raise Exception + + return yaml.safe_load(open(config).read())['options'] + + +def get_default_config(): + ''' + Load default charm config from config.yaml return as a dict. + If no default is set in config.yaml, its value is None. + ''' + default_config = {} + config = load_config() + for k, v in config.iteritems(): + if 'default' in v: + default_config[k] = v['default'] + else: + default_config[k] = None + return default_config + + +class CharmTestCase(unittest.TestCase): + def setUp(self, obj, patches): + super(CharmTestCase, self).setUp() + self.patches = patches + self.obj = obj + self.test_config = TestConfig() + self.test_relation = TestRelation() + self.patch_all() + + def patch(self, method): + _m = patch.object(self.obj, method) + mock = _m.start() + self.addCleanup(_m.stop) + return mock + + def patch_all(self): + for method in self.patches: + setattr(self, method, self.patch(method)) + + +class TestConfig(object): + def __init__(self): + self.config = get_default_config() + + def get(self, attr): + try: + return self.config[attr] + except KeyError: + return None + + def get_all(self): + return self.config + + def set(self, attr, value): + if attr not in self.config: + raise KeyError + self.config[attr] = value + + +class TestRelation(object): + def __init__(self, relation_data={}): + self.relation_data = relation_data + + def set(self, relation_data): + self.relation_data = relation_data + + def get(self, attr=None, unit=None, rid=None): + if attr is None: + return self.relation_data + elif attr in self.relation_data: + return self.relation_data[attr] + return None