import collections import copy import json import mock import six import unittest import yaml from mock import ( patch, Mock, MagicMock, call ) from tests.helpers import patch_open import tests.utils import charmhelpers.contrib.openstack.context as context if not six.PY3: open_builtin = '__builtin__.open' else: open_builtin = 'builtins.open' class FakeRelation(object): ''' A fake relation class. Lets tests specify simple relation data for a default relation + unit (foo:0, foo/0, set in setUp()), eg: rel = { 'private-address': 'foo', 'password': 'passwd', } relation = FakeRelation(rel) self.relation_get.side_effect = relation.get passwd = self.relation_get('password') or more complex relations meant to be addressed by explicit relation id + unit id combos: rel = { 'mysql:0': { 'mysql/0': { 'private-address': 'foo', 'password': 'passwd', } } } relation = FakeRelation(rel) self.relation_get.side_affect = relation.get passwd = self.relation_get('password', rid='mysql:0', unit='mysql/0') ''' def __init__(self, relation_data): self.relation_data = relation_data def get(self, attribute=None, unit=None, rid=None): if not rid or rid == 'foo:0': if attribute is None: return self.relation_data elif attribute in self.relation_data: return self.relation_data[attribute] return None else: if rid not in self.relation_data: return None try: relation = self.relation_data[rid][unit] except KeyError: return None if attribute is None: return relation if attribute in relation: return relation[attribute] return None def relation_ids(self, relation): rids = [] for rid in sorted(self.relation_data.keys()): if relation + ':' in rid: rids.append(rid) return rids def relation_units(self, relation_id): if relation_id not in self.relation_data: return None return sorted(self.relation_data[relation_id].keys()) SHARED_DB_RELATION = { 'db_host': 'dbserver.local', 'password': 'foo' } SHARED_DB_RELATION_W_PORT = { 'db_host': 'dbserver.local', 'password': 'foo', 'db_port': 3306, } SHARED_DB_RELATION_ALT_RID = { 'mysql-alt:0': { 'mysql-alt/0': { 'db_host': 'dbserver-alt.local', 'password': 'flump'}}} SHARED_DB_RELATION_SSL = { 'db_host': 'dbserver.local', 'password': 'foo', 'ssl_ca': 'Zm9vCg==', 'ssl_cert': 'YmFyCg==', 'ssl_key': 'Zm9vYmFyCg==', } SHARED_DB_CONFIG = { 'database-user': 'adam', 'database': 'foodb', } SHARED_DB_RELATION_NAMESPACED = { 'db_host': 'bar', 'quantum_password': 'bar2' } SHARED_DB_RELATION_ACCESS_NETWORK = { 'db_host': 'dbserver.local', 'password': 'foo', 'access-network': '10.5.5.0/24', 'hostname': 'bar', } IDENTITY_SERVICE_RELATION_HTTP = { 'service_port': '5000', 'service_host': 'keystonehost.local', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'internal_host': 'keystone-internal.local', 'internal_port': '5000', 'service_domain': 'admin_domain', 'service_tenant': 'admin', 'service_tenant_id': '123456', 'service_password': 'foo', 'service_username': 'adam', 'service_protocol': 'http', 'auth_protocol': 'http', 'internal_protocol': 'http', } IDENTITY_SERVICE_RELATION_UNSET = { 'service_port': '5000', 'service_host': 'keystonehost.local', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'internal_host': 'keystone-internal.local', 'internal_port': '5000', 'service_domain': 'admin_domain', 'service_tenant': 'admin', 'service_password': 'foo', 'service_username': 'adam', } IDENTITY_CREDENTIALS_RELATION_UNSET = { 'credentials_port': '5000', 'credentials_host': 'keystonehost.local', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'auth_protocol': 'https', 'domain': 'admin_domain', 'credentials_project': 'admin', 'credentials_project_id': '123456', 'credentials_password': 'foo', 'credentials_username': 'adam', 'credentials_protocol': 'https', } APIIDENTITY_SERVICE_RELATION_UNSET = { 'neutron-plugin-api:0': { 'neutron-api/0': { 'service_port': '5000', 'service_host': 'keystonehost.local', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'internal_port': '5000', 'internal_host': 'keystone-internal.local', 'service_domain': 'admin_domain', 'service_tenant': 'admin', 'service_password': 'foo', 'service_username': 'adam', } } } IDENTITY_SERVICE_RELATION_HTTPS = { 'service_port': '5000', 'service_host': 'keystonehost.local', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'internal_host': 'keystone-internal.local', 'internal_port': '5000', 'service_domain': 'admin_domain', 'service_tenant': 'admin', 'service_password': 'foo', 'service_username': 'adam', 'service_protocol': 'https', 'auth_protocol': 'https', 'internal_protocol': 'https', } IDENTITY_SERVICE_RELATION_VERSIONED = { 'api_version': '3', 'service_tenant_id': 'svc-proj-id', 'service_domain_id': 'svc-dom-id', } IDENTITY_SERVICE_RELATION_VERSIONED.update(IDENTITY_SERVICE_RELATION_HTTPS) IDENTITY_CREDENTIALS_RELATION_VERSIONED = { 'api_version': '3', 'service_tenant_id': 'svc-proj-id', 'service_domain_id': 'svc-dom-id', } IDENTITY_CREDENTIALS_RELATION_VERSIONED.update(IDENTITY_CREDENTIALS_RELATION_UNSET) POSTGRESQL_DB_RELATION = { 'host': 'dbserver.local', 'user': 'adam', 'password': 'foo', } POSTGRESQL_DB_CONFIG = { 'database': 'foodb', } IDENTITY_SERVICE_RELATION = { 'service_port': '5000', 'service_host': 'keystonehost.local', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'service_domain': 'admin_domain', 'service_tenant': 'admin', 'service_password': 'foo', 'service_username': 'adam', } AMQP_RELATION = { 'private-address': 'rabbithost', 'password': 'foobar', 'vip': '10.0.0.1', } AMQP_RELATION_ALT_RID = { 'amqp-alt:0': { 'rabbitmq-alt/0': { 'private-address': 'rabbitalthost1', 'password': 'flump', }, } } AMQP_RELATION_WITH_SSL = { 'private-address': 'rabbithost', 'password': 'foobar', 'vip': '10.0.0.1', 'ssl_port': 5671, 'ssl_ca': 'cert', 'ha_queues': 'queues', } AMQP_AA_RELATION = { 'amqp:0': { 'rabbitmq/0': { 'private-address': 'rabbithost1', 'password': 'foobar', }, 'rabbitmq/1': { 'private-address': 'rabbithost2', 'password': 'foobar', }, 'rabbitmq/2': { # Should be ignored because password is missing. 'private-address': 'rabbithost3', } } } AMQP_CONFIG = { 'rabbit-user': 'adam', 'rabbit-vhost': 'foo', } AMQP_OSLO_CONFIG = { 'oslo-messaging-flags': ("rabbit_max_retries=1" ",rabbit_retry_backoff=1" ",rabbit_retry_interval=1"), 'oslo-messaging-driver': 'log' } AMQP_NOTIFICATION_FORMAT = { 'notification-format': 'both' } AMQP_NOTIFICATION_TOPICS = { 'notification-topics': 'foo,bar' } AMQP_NOTIFICATIONS_LOGS = { 'send-notifications-to-logs': True } AMQP_NOVA_CONFIG = { 'nova-rabbit-user': 'adam', 'nova-rabbit-vhost': 'foo', } HAPROXY_CONFIG = { 'haproxy-server-timeout': 50000, 'haproxy-client-timeout': 50000, } CEPH_RELATION = { 'ceph:0': { 'ceph/0': { 'private-address': 'ceph_node1', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true' }, 'ceph/1': { 'private-address': 'ceph_node2', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'false' }, } } CEPH_RELATION_WITH_PUBLIC_ADDR = { 'ceph:0': { 'ceph/0': { 'ceph-public-address': '192.168.1.10', 'private-address': 'ceph_node1', 'auth': 'foo', 'key': 'bar' }, 'ceph/1': { 'ceph-public-address': '192.168.1.11', 'private-address': 'ceph_node2', 'auth': 'foo', 'key': 'bar' }, } } CEPH_REL_WITH_PUBLIC_ADDR_PORT = { 'ceph:0': { 'ceph/0': { 'ceph-public-address': '192.168.1.10:1234', 'private-address': 'ceph_node1', 'auth': 'foo', 'key': 'bar' }, 'ceph/1': { 'ceph-public-address': '192.168.1.11:4321', 'private-address': 'ceph_node2', 'auth': 'foo', 'key': 'bar' }, } } CEPH_REL_WITH_PUBLIC_IPv6_ADDR = { 'ceph:0': { 'ceph/0': { 'ceph-public-address': '2001:5c0:9168::1', 'private-address': 'ceph_node1', 'auth': 'foo', 'key': 'bar' }, 'ceph/1': { 'ceph-public-address': '2001:5c0:9168::2', 'private-address': 'ceph_node2', 'auth': 'foo', 'key': 'bar' }, } } CEPH_REL_WITH_PUBLIC_IPv6_ADDR_PORT = { 'ceph:0': { 'ceph/0': { 'ceph-public-address': '[2001:5c0:9168::1]:1234', 'private-address': 'ceph_node1', 'auth': 'foo', 'key': 'bar' }, 'ceph/1': { 'ceph-public-address': '[2001:5c0:9168::2]:4321', 'private-address': 'ceph_node2', 'auth': 'foo', 'key': 'bar' }, } } CEPH_REL_WITH_MULTI_PUBLIC_ADDR = { 'ceph:0': { 'ceph/0': { 'ceph-public-address': '192.168.1.10 192.168.1.20', 'private-address': 'ceph_node1', 'auth': 'foo', 'key': 'bar' }, 'ceph/1': { 'ceph-public-address': '192.168.1.11 192.168.1.21', 'private-address': 'ceph_node2', 'auth': 'foo', 'key': 'bar' }, } } CEPH_REL_WITH_DEFAULT_FEATURES = { 'ceph:0': { 'ceph/0': { 'private-address': 'ceph_node1', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', 'rbd-features': '1' }, 'ceph/1': { 'private-address': 'ceph_node2', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'false', 'rbd-features': '1' }, } } IDENTITY_RELATION_NO_CERT = { 'identity-service:0': { 'keystone/0': { 'private-address': 'keystone1', }, } } IDENTITY_RELATION_SINGLE_CERT = { 'identity-service:0': { 'keystone/0': { 'private-address': 'keystone1', 'ssl_cert_cinderhost1': 'certa', 'ssl_key_cinderhost1': 'keya', }, } } IDENTITY_RELATION_MULTIPLE_CERT = { 'identity-service:0': { 'keystone/0': { 'private-address': 'keystone1', 'ssl_cert_cinderhost1-int-network': 'certa', 'ssl_key_cinderhost1-int-network': 'keya', 'ssl_cert_cinderhost1-pub-network': 'certa', 'ssl_key_cinderhost1-pub-network': 'keya', 'ssl_cert_cinderhost1-adm-network': 'certa', 'ssl_key_cinderhost1-adm-network': 'keya', }, } } QUANTUM_NETWORK_SERVICE_RELATION = { 'quantum-network-service:0': { 'unit/0': { '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' }, } } QUANTUM_NETWORK_SERVICE_RELATION_VERSIONED = { 'quantum-network-service:0': { 'unit/0': { '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', 'api_version': '3', }, } } SUB_CONFIG = """ nova: /etc/nova/nova.conf: sections: DEFAULT: - [nova-key1, value1] - [nova-key2, value2] glance: /etc/glance/glance.conf: sections: DEFAULT: - [glance-key1, value1] - [glance-key2, value2] """ NOVA_SUB_CONFIG1 = """ nova: /etc/nova/nova.conf: sections: DEFAULT: - [nova-key1, value1] - [nova-key2, value2] """ NOVA_SUB_CONFIG2 = """ nova-compute: /etc/nova/nova.conf: sections: DEFAULT: - [nova-key3, value3] - [nova-key4, value4] """ NOVA_SUB_CONFIG3 = """ nova-compute: /etc/nova/nova.conf: sections: DEFAULT: - [nova-key5, value5] - [nova-key6, value6] """ CINDER_SUB_CONFIG1 = """ cinder: /etc/cinder/cinder.conf: sections: cinder-1-section: - [key1, value1] """ CINDER_SUB_CONFIG2 = """ cinder: /etc/cinder/cinder.conf: sections: cinder-2-section: - [key2, value2] not-a-section: 1234 """ SUB_CONFIG_RELATION = { 'nova-subordinate:0': { 'nova-subordinate/0': { 'private-address': 'nova_node1', 'subordinate_configuration': json.dumps(yaml.safe_load(SUB_CONFIG)), }, }, 'glance-subordinate:0': { 'glance-subordinate/0': { 'private-address': 'glance_node1', 'subordinate_configuration': json.dumps(yaml.safe_load(SUB_CONFIG)), }, }, 'foo-subordinate:0': { 'foo-subordinate/0': { 'private-address': 'foo_node1', 'subordinate_configuration': 'ea8e09324jkadsfh', }, }, 'cinder-subordinate:0': { 'cinder-subordinate/0': { 'private-address': 'cinder_node1', 'subordinate_configuration': json.dumps( yaml.safe_load(CINDER_SUB_CONFIG1)), }, }, 'cinder-subordinate:1': { 'cinder-subordinate/1': { 'private-address': 'cinder_node1', 'subordinate_configuration': json.dumps( yaml.safe_load(CINDER_SUB_CONFIG2)), }, }, 'empty:0': {}, } SUB_CONFIG_RELATION2 = { 'nova-ceilometer:6': { 'ceilometer-agent/0': { 'private-address': 'nova_node1', 'subordinate_configuration': json.dumps( yaml.safe_load(NOVA_SUB_CONFIG1)), }, }, 'neutron-plugin:3': { 'neutron-ovs-plugin/0': { 'private-address': 'nova_node1', 'subordinate_configuration': json.dumps( yaml.safe_load(NOVA_SUB_CONFIG2)), }, }, 'neutron-plugin:4': { 'neutron-other-plugin/0': { 'private-address': 'nova_node1', 'subordinate_configuration': json.dumps( yaml.safe_load(NOVA_SUB_CONFIG3)), }, } } NONET_CONFIG = { 'vip': 'cinderhost1vip', 'os-internal-network': None, 'os-admin-network': None, 'os-public-network': None } FULLNET_CONFIG = { 'vip': '10.5.1.1 10.5.2.1 10.5.3.1', 'os-internal-network': "10.5.1.0/24", 'os-admin-network': "10.5.2.0/24", 'os-public-network': "10.5.3.0/24" } MACHINE_MACS = { 'eth0': 'fe:c5:ce:8e:2b:00', 'eth1': 'fe:c5:ce:8e:2b:01', 'eth2': 'fe:c5:ce:8e:2b:02', 'eth3': 'fe:c5:ce:8e:2b:03', } MACHINE_NICS = { 'eth0': ['192.168.0.1'], 'eth1': ['192.168.0.2'], 'eth2': [], 'eth3': [], } ABSENT_MACS = "aa:a5:ae:ae:ab:a4 " # Imported in contexts.py and needs patching in setUp() TO_PATCH = [ 'b64decode', 'check_call', 'get_cert', 'get_ca_cert', 'install_ca_cert', 'log', 'config', 'relation_get', 'relation_ids', 'related_units', 'is_relation_made', 'relation_set', 'local_address', 'https', 'determine_api_port', 'determine_apache_port', 'is_clustered', 'time', 'https', 'get_address_in_network', 'get_netmask_for_address', 'local_unit', 'get_ipv6_addr', 'mkdir', 'write_file', 'get_relation_ip', 'charm_name', 'sysctl_create', 'kv', 'pwgen', 'lsb_release', 'network_get_primary_address', 'resolve_address', 'is_ipv6_disabled', ] class fake_config(object): def __init__(self, data): self.data = data def __call__(self, attr): if attr in self.data: return self.data[attr] return None class fake_is_relation_made(): def __init__(self, relations): self.relations = relations def rel_made(self, relation): return self.relations[relation] class TestDB(object): '''Test KV store for unitdata testing''' def __init__(self): self.data = {} self.flushed = False def get(self, key, default=None): return self.data.get(key, default) def set(self, key, value): self.data[key] = value return value def flush(self): self.flushed = True class ContextTests(unittest.TestCase): def setUp(self): for m in TO_PATCH: setattr(self, m, self._patch(m)) # mock at least a single relation + unit self.relation_ids.return_value = ['foo:0'] self.related_units.return_value = ['foo/0'] self.local_unit.return_value = 'localunit' self.kv.side_effect = TestDB self.pwgen.return_value = 'testpassword' self.lsb_release.return_value = {'DISTRIB_RELEASE': '16.04'} self.network_get_primary_address.side_effect = NotImplementedError() self.resolve_address.return_value = '10.5.1.50' self.maxDiff = None def _patch(self, method): _m = patch('charmhelpers.contrib.openstack.context.' + method) mock = _m.start() self.addCleanup(_m.stop) return mock def test_base_class_not_implemented(self): base = context.OSContextGenerator() self.assertRaises(NotImplementedError, base) @patch.object(context, 'get_os_codename_install_source') def test_shared_db_context_with_data(self, os_codename): '''Test shared-db context with all required data''' os_codename.return_value = 'queens' relation = FakeRelation(relation_data=SHARED_DB_RELATION) self.relation_get.side_effect = relation.get self.get_address_in_network.return_value = '' self.config.side_effect = fake_config(SHARED_DB_CONFIG) shared_db = context.SharedDBContext() result = shared_db() expected = { 'database_host': 'dbserver.local', 'database': 'foodb', 'database_user': 'adam', 'database_password': 'foo', 'database_type': 'mysql+pymysql', } self.assertEquals(result, expected) def test_shared_db_context_with_data_and_access_net_mismatch(self): """Mismatch between hostname and hostname for access net - defers execution""" relation = FakeRelation( relation_data=SHARED_DB_RELATION_ACCESS_NETWORK) self.relation_get.side_effect = relation.get self.get_address_in_network.return_value = '10.5.5.1' self.config.side_effect = fake_config(SHARED_DB_CONFIG) shared_db = context.SharedDBContext() result = shared_db() self.assertEquals(result, None) self.relation_set.assert_called_with( relation_settings={ 'hostname': '10.5.5.1'}) @patch.object(context, 'get_os_codename_install_source') def test_shared_db_context_with_data_and_access_net_match(self, os_codename): """Correctly set hostname for access net returns complete context""" os_codename.return_value = 'queens' relation = FakeRelation( relation_data=SHARED_DB_RELATION_ACCESS_NETWORK) self.relation_get.side_effect = relation.get self.get_address_in_network.return_value = 'bar' self.config.side_effect = fake_config(SHARED_DB_CONFIG) shared_db = context.SharedDBContext() result = shared_db() expected = { 'database_host': 'dbserver.local', 'database': 'foodb', 'database_user': 'adam', 'database_password': 'foo', 'database_type': 'mysql+pymysql', } self.assertEquals(result, expected) @patch.object(context, 'get_os_codename_install_source') def test_shared_db_context_explicit_relation_id(self, os_codename): '''Test shared-db context setting the relation_id''' os_codename.return_value = 'queens' relation = FakeRelation(relation_data=SHARED_DB_RELATION_ALT_RID) self.related_units.return_value = ['mysql-alt/0'] self.relation_get.side_effect = relation.get self.get_address_in_network.return_value = '' self.config.side_effect = fake_config(SHARED_DB_CONFIG) shared_db = context.SharedDBContext(relation_id='mysql-alt:0') result = shared_db() expected = { 'database_host': 'dbserver-alt.local', 'database': 'foodb', 'database_user': 'adam', 'database_password': 'flump', 'database_type': 'mysql+pymysql', } self.assertEquals(result, expected) @patch.object(context, 'get_os_codename_install_source') def test_shared_db_context_with_port(self, os_codename): '''Test shared-db context with all required data''' os_codename.return_value = 'queens' relation = FakeRelation(relation_data=SHARED_DB_RELATION_W_PORT) self.relation_get.side_effect = relation.get self.get_address_in_network.return_value = '' self.config.side_effect = fake_config(SHARED_DB_CONFIG) shared_db = context.SharedDBContext() result = shared_db() expected = { 'database_host': 'dbserver.local', 'database': 'foodb', 'database_user': 'adam', 'database_password': 'foo', 'database_type': 'mysql+pymysql', 'database_port': 3306, } self.assertEquals(result, expected) @patch('os.path.exists') @patch(open_builtin) def test_db_ssl(self, _open, osexists): osexists.return_value = False ssl_dir = '/etc/dbssl' db_ssl_ctxt = context.db_ssl(SHARED_DB_RELATION_SSL, {}, ssl_dir) expected = { 'database_ssl_ca': ssl_dir + '/db-client.ca', 'database_ssl_cert': ssl_dir + '/db-client.cert', 'database_ssl_key': ssl_dir + '/db-client.key', } files = [ call(expected['database_ssl_ca'], 'wb'), call(expected['database_ssl_cert'], 'wb'), call(expected['database_ssl_key'], 'wb') ] for f in files: self.assertIn(f, _open.call_args_list) self.assertEquals(db_ssl_ctxt, expected) decode = [ call(SHARED_DB_RELATION_SSL['ssl_ca']), call(SHARED_DB_RELATION_SSL['ssl_cert']), call(SHARED_DB_RELATION_SSL['ssl_key']) ] self.assertEquals(decode, self.b64decode.call_args_list) def test_db_ssl_nossldir(self): db_ssl_ctxt = context.db_ssl(SHARED_DB_RELATION_SSL, {}, None) self.assertEquals(db_ssl_ctxt, {}) @patch.object(context, 'get_os_codename_install_source') def test_shared_db_context_with_missing_relation(self, os_codename): '''Test shared-db context missing relation data''' os_codename.return_value = 'stein' incomplete_relation = copy.copy(SHARED_DB_RELATION) incomplete_relation['password'] = None relation = FakeRelation(relation_data=incomplete_relation) self.relation_get.side_effect = relation.get self.config.return_value = SHARED_DB_CONFIG shared_db = context.SharedDBContext() result = shared_db() self.assertEquals(result, {}) def test_shared_db_context_with_missing_config(self): '''Test shared-db context missing relation data''' incomplete_config = copy.copy(SHARED_DB_CONFIG) del incomplete_config['database-user'] self.config.side_effect = fake_config(incomplete_config) relation = FakeRelation(relation_data=SHARED_DB_RELATION) self.relation_get.side_effect = relation.get self.config.return_value = incomplete_config shared_db = context.SharedDBContext() self.assertRaises(context.OSContextError, shared_db) @patch.object(context, 'get_os_codename_install_source') def test_shared_db_context_with_params(self, os_codename): '''Test shared-db context with object parameters''' os_codename.return_value = 'stein' shared_db = context.SharedDBContext( database='quantum', user='quantum', relation_prefix='quantum') relation = FakeRelation(relation_data=SHARED_DB_RELATION_NAMESPACED) self.relation_get.side_effect = relation.get result = shared_db() self.assertIn( call(rid='foo:0', unit='foo/0'), self.relation_get.call_args_list) self.assertEquals( result, {'database': 'quantum', 'database_user': 'quantum', 'database_password': 'bar2', 'database_host': 'bar', 'database_type': 'mysql+pymysql'}) @patch.object(context, 'get_os_codename_install_source') def test_shared_db_context_with_params_pike(self, os_codename): '''Test shared-db context with object parameters''' os_codename.return_value = 'pike' shared_db = context.SharedDBContext( database='quantum', user='quantum', relation_prefix='quantum') relation = FakeRelation(relation_data=SHARED_DB_RELATION_NAMESPACED) self.relation_get.side_effect = relation.get result = shared_db() self.assertIn( call(rid='foo:0', unit='foo/0'), self.relation_get.call_args_list) self.assertEquals( result, {'database': 'quantum', 'database_user': 'quantum', 'database_password': 'bar2', 'database_host': 'bar', 'database_type': 'mysql'}) @patch.object(context, 'get_os_codename_install_source') @patch('charmhelpers.contrib.openstack.context.format_ipv6_addr') def test_shared_db_context_with_ipv6(self, format_ipv6_addr, os_codename): '''Test shared-db context with ipv6''' shared_db = context.SharedDBContext( database='quantum', user='quantum', relation_prefix='quantum') os_codename.return_value = 'stein' relation = FakeRelation(relation_data=SHARED_DB_RELATION_NAMESPACED) self.relation_get.side_effect = relation.get format_ipv6_addr.return_value = '[2001:db8:1::1]' result = shared_db() self.assertIn( call(rid='foo:0', unit='foo/0'), self.relation_get.call_args_list) self.assertEquals( result, {'database': 'quantum', 'database_user': 'quantum', 'database_password': 'bar2', 'database_host': '[2001:db8:1::1]', 'database_type': 'mysql+pymysql'}) def test_postgresql_db_context_with_data(self): '''Test postgresql-db context with all required data''' relation = FakeRelation(relation_data=POSTGRESQL_DB_RELATION) self.relation_get.side_effect = relation.get self.config.side_effect = fake_config(POSTGRESQL_DB_CONFIG) postgresql_db = context.PostgresqlDBContext() result = postgresql_db() expected = { 'database_host': 'dbserver.local', 'database': 'foodb', 'database_user': 'adam', 'database_password': 'foo', 'database_type': 'postgresql', } self.assertEquals(result, expected) def test_postgresql_db_context_with_missing_relation(self): '''Test postgresql-db context missing relation data''' incomplete_relation = copy.copy(POSTGRESQL_DB_RELATION) incomplete_relation['password'] = None relation = FakeRelation(relation_data=incomplete_relation) self.relation_get.side_effect = relation.get self.config.return_value = POSTGRESQL_DB_CONFIG postgresql_db = context.PostgresqlDBContext() result = postgresql_db() self.assertEquals(result, {}) def test_postgresql_db_context_with_missing_config(self): '''Test postgresql-db context missing relation data''' incomplete_config = copy.copy(POSTGRESQL_DB_CONFIG) del incomplete_config['database'] self.config.side_effect = fake_config(incomplete_config) relation = FakeRelation(relation_data=POSTGRESQL_DB_RELATION) self.relation_get.side_effect = relation.get self.config.return_value = incomplete_config postgresql_db = context.PostgresqlDBContext() self.assertRaises(context.OSContextError, postgresql_db) def test_postgresql_db_context_with_params(self): '''Test postgresql-db context with object parameters''' postgresql_db = context.PostgresqlDBContext(database='quantum') result = postgresql_db() self.assertEquals(result['database'], 'quantum') @patch.object(context, 'filter_installed_packages', return_value=[]) @patch.object(context, 'os_release', return_value='rocky') def test_identity_service_context_with_data(self, *args): '''Test shared-db context with all required data''' relation = FakeRelation(relation_data=IDENTITY_SERVICE_RELATION_UNSET) self.relation_get.side_effect = relation.get identity_service = context.IdentityServiceContext() result = identity_service() expected = { 'admin_password': 'foo', 'admin_tenant_name': 'admin', 'admin_tenant_id': None, 'admin_domain_id': None, 'admin_user': 'adam', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'auth_protocol': 'http', 'service_host': 'keystonehost.local', 'service_port': '5000', 'service_protocol': 'http', 'internal_host': 'keystone-internal.local', 'internal_port': '5000', 'internal_protocol': 'http', 'api_version': '2.0', } result.pop('keystone_authtoken') self.assertEquals(result, expected) def test_identity_credentials_context_with_data(self): '''Test identity-credentials context with all required data''' relation = FakeRelation(relation_data=IDENTITY_CREDENTIALS_RELATION_UNSET) self.relation_get.side_effect = relation.get identity_credentials = context.IdentityCredentialsContext() result = identity_credentials() expected = { 'admin_password': 'foo', 'admin_tenant_name': 'admin', 'admin_tenant_id': '123456', 'admin_user': 'adam', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'auth_protocol': 'https', 'service_host': 'keystonehost.local', 'service_port': '5000', 'service_protocol': 'https', 'api_version': '2.0', } self.assertEquals(result, expected) @patch.object(context, 'filter_installed_packages', return_value=[]) @patch.object(context, 'os_release', return_value='rocky') def test_identity_service_context_with_altname(self, *args): '''Test identity context when using an explicit relation name''' relation = FakeRelation( relation_data=APIIDENTITY_SERVICE_RELATION_UNSET ) self.relation_get.side_effect = relation.get self.relation_ids.return_value = ['neutron-plugin-api:0'] self.related_units.return_value = ['neutron-api/0'] identity_service = context.IdentityServiceContext( rel_name='neutron-plugin-api' ) result = identity_service() expected = { 'admin_password': 'foo', 'admin_tenant_name': 'admin', 'admin_tenant_id': None, 'admin_domain_id': None, 'admin_user': 'adam', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'auth_protocol': 'http', 'service_host': 'keystonehost.local', 'service_port': '5000', 'service_protocol': 'http', 'internal_host': 'keystone-internal.local', 'internal_port': '5000', 'internal_protocol': 'http', 'api_version': '2.0', } result.pop('keystone_authtoken') self.assertEquals(result, expected) @patch.object(context, 'filter_installed_packages', return_value=[]) @patch.object(context, 'os_release', return_value='rocky') def test_identity_service_context_with_cache(self, *args): '''Test shared-db context with signing cache info''' relation = FakeRelation(relation_data=IDENTITY_SERVICE_RELATION_UNSET) self.relation_get.side_effect = relation.get svc = 'cinder' identity_service = context.IdentityServiceContext(service=svc, service_user=svc) result = identity_service() expected = { 'admin_password': 'foo', 'admin_tenant_name': 'admin', 'admin_tenant_id': None, 'admin_domain_id': None, 'admin_user': 'adam', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'auth_protocol': 'http', 'service_host': 'keystonehost.local', 'service_port': '5000', 'service_protocol': 'http', 'internal_host': 'keystone-internal.local', 'internal_port': '5000', 'internal_protocol': 'http', 'signing_dir': '/var/cache/cinder', 'api_version': '2.0', } self.assertTrue(self.mkdir.called) result.pop('keystone_authtoken') self.assertEquals(result, expected) @patch.object(context, 'filter_installed_packages', return_value=[]) @patch.object(context, 'os_release', return_value='rocky') def test_identity_service_context_with_data_http(self, *args): '''Test shared-db context with all required data''' relation = FakeRelation(relation_data=IDENTITY_SERVICE_RELATION_HTTP) self.relation_get.side_effect = relation.get identity_service = context.IdentityServiceContext() result = identity_service() expected = { 'admin_password': 'foo', 'admin_tenant_name': 'admin', 'admin_tenant_id': '123456', 'admin_domain_id': None, 'admin_user': 'adam', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'auth_protocol': 'http', 'service_host': 'keystonehost.local', 'service_port': '5000', 'service_protocol': 'http', 'internal_host': 'keystone-internal.local', 'internal_port': '5000', 'internal_protocol': 'http', 'api_version': '2.0', } result.pop('keystone_authtoken') self.assertEquals(result, expected) @patch.object(context, 'filter_installed_packages', return_value=[]) @patch.object(context, 'os_release', return_value='rocky') def test_identity_service_context_with_data_https(self, *args): '''Test shared-db context with all required data''' relation = FakeRelation(relation_data=IDENTITY_SERVICE_RELATION_HTTPS) self.relation_get.side_effect = relation.get identity_service = context.IdentityServiceContext() result = identity_service() expected = { 'admin_password': 'foo', 'admin_tenant_name': 'admin', 'admin_tenant_id': None, 'admin_domain_id': None, 'admin_user': 'adam', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'auth_protocol': 'https', 'service_host': 'keystonehost.local', 'service_port': '5000', 'service_protocol': 'https', 'internal_host': 'keystone-internal.local', 'internal_port': '5000', 'internal_protocol': 'https', 'api_version': '2.0', } result.pop('keystone_authtoken') self.assertEquals(result, expected) @patch.object(context, 'filter_installed_packages', return_value=[]) @patch.object(context, 'os_release', return_value='rocky') def test_identity_service_context_with_data_versioned(self, *args): '''Test shared-db context with api version supplied from keystone''' relation = FakeRelation( relation_data=IDENTITY_SERVICE_RELATION_VERSIONED) self.relation_get.side_effect = relation.get identity_service = context.IdentityServiceContext() result = identity_service() expected = { 'admin_password': 'foo', 'admin_domain_name': 'admin_domain', 'admin_tenant_name': 'admin', 'admin_tenant_id': 'svc-proj-id', 'admin_domain_id': 'svc-dom-id', 'service_project_id': 'svc-proj-id', 'service_domain_id': 'svc-dom-id', 'admin_user': 'adam', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'auth_protocol': 'https', 'service_host': 'keystonehost.local', 'service_port': '5000', 'service_protocol': 'https', 'internal_host': 'keystone-internal.local', 'internal_port': '5000', 'internal_protocol': 'https', 'api_version': '3', } result.pop('keystone_authtoken') self.assertEquals(result, expected) def test_identity_credentials_context_with_data_versioned(self): '''Test identity-credentials context with api version supplied from keystone''' relation = FakeRelation( relation_data=IDENTITY_CREDENTIALS_RELATION_VERSIONED) self.relation_get.side_effect = relation.get identity_credentials = context.IdentityCredentialsContext() result = identity_credentials() expected = { 'admin_password': 'foo', 'admin_domain_name': 'admin_domain', 'admin_tenant_name': 'admin', 'admin_tenant_id': '123456', 'admin_user': 'adam', 'auth_host': 'keystone-host.local', 'auth_port': '35357', 'auth_protocol': 'https', 'service_host': 'keystonehost.local', 'service_port': '5000', 'service_protocol': 'https', 'api_version': '3', } self.assertEquals(result, expected) @patch.object(context, 'filter_installed_packages', return_value=[]) @patch.object(context, 'os_release', return_value='rocky') @patch('charmhelpers.contrib.openstack.context.format_ipv6_addr') def test_identity_service_context_with_ipv6(self, format_ipv6_addr, *args): '''Test identity-service context with ipv6''' relation = FakeRelation(relation_data=IDENTITY_SERVICE_RELATION_HTTP) self.relation_get.side_effect = relation.get format_ipv6_addr.return_value = '[2001:db8:1::1]' identity_service = context.IdentityServiceContext() result = identity_service() expected = { 'admin_password': 'foo', 'admin_tenant_name': 'admin', 'admin_tenant_id': '123456', 'admin_domain_id': None, 'admin_user': 'adam', 'auth_host': '[2001:db8:1::1]', 'auth_port': '35357', 'auth_protocol': 'http', 'service_host': '[2001:db8:1::1]', 'service_port': '5000', 'service_protocol': 'http', 'internal_host': '[2001:db8:1::1]', 'internal_port': '5000', 'internal_protocol': 'http', 'api_version': '2.0', } result.pop('keystone_authtoken') self.assertEquals(result, expected) @patch.object(context, 'filter_installed_packages', return_value=[]) @patch.object(context, 'os_release', return_value='rocky') def test_identity_service_context_with_missing_relation(self, *args): '''Test shared-db context missing relation data''' incomplete_relation = copy.copy(IDENTITY_SERVICE_RELATION_UNSET) incomplete_relation['service_password'] = None relation = FakeRelation(relation_data=incomplete_relation) self.relation_get.side_effect = relation.get identity_service = context.IdentityServiceContext() result = identity_service() self.assertEquals(result, {}) @patch.object(context, 'filter_installed_packages') @patch.object(context, 'os_release') def test_keystone_authtoken_www_authenticate_uri_stein_apiv3(self, mock_os_release, mock_filter_installed_packages): relation_data = copy.deepcopy(IDENTITY_SERVICE_RELATION_VERSIONED) relation = FakeRelation(relation_data=relation_data) self.relation_get.side_effect = relation.get mock_filter_installed_packages.return_value = [] mock_os_release.return_value = 'stein' identity_service = context.IdentityServiceContext() cfg_ctx = identity_service() keystone_authtoken = cfg_ctx.get('keystone_authtoken', {}) expected = collections.OrderedDict(( ('auth_type', 'password'), ('www_authenticate_uri', 'https://keystonehost.local:5000/v3'), ('auth_url', 'https://keystone-host.local:35357/v3'), ('project_domain_name', 'admin_domain'), ('user_domain_name', 'admin_domain'), ('project_name', 'admin'), ('username', 'adam'), ('password', 'foo'), ('signing_dir', ''), )) self.assertEquals(keystone_authtoken, expected) def test_amqp_context_with_data(self): '''Test amqp context with all required data''' relation = FakeRelation(relation_data=AMQP_RELATION) self.relation_get.side_effect = relation.get self.config.return_value = AMQP_CONFIG amqp = context.AMQPContext() result = amqp() expected = { 'oslo_messaging_driver': 'messagingv2', 'rabbitmq_host': 'rabbithost', 'rabbitmq_password': 'foobar', 'rabbitmq_user': 'adam', 'rabbitmq_virtual_host': 'foo', 'transport_url': 'rabbit://adam:foobar@rabbithost:5672/foo' } self.assertEquals(result, expected) def test_amqp_context_explicit_relation_id(self): '''Test amqp context setting the relation_id''' relation = FakeRelation(relation_data=AMQP_RELATION_ALT_RID) self.relation_get.side_effect = relation.get self.related_units.return_value = ['rabbitmq-alt/0'] self.config.return_value = AMQP_CONFIG amqp = context.AMQPContext(relation_id='amqp-alt:0') result = amqp() expected = { 'oslo_messaging_driver': 'messagingv2', 'rabbitmq_host': 'rabbitalthost1', 'rabbitmq_password': 'flump', 'rabbitmq_user': 'adam', 'rabbitmq_virtual_host': 'foo', 'transport_url': 'rabbit://adam:flump@rabbitalthost1:5672/foo' } self.assertEquals(result, expected) def test_amqp_context_with_data_altname(self): '''Test amqp context with alternative relation name''' relation = FakeRelation(relation_data=AMQP_RELATION) self.relation_get.side_effect = relation.get self.config.return_value = AMQP_NOVA_CONFIG amqp = context.AMQPContext( rel_name='amqp-nova', relation_prefix='nova') result = amqp() expected = { 'oslo_messaging_driver': 'messagingv2', 'rabbitmq_host': 'rabbithost', 'rabbitmq_password': 'foobar', 'rabbitmq_user': 'adam', 'rabbitmq_virtual_host': 'foo', 'transport_url': 'rabbit://adam:foobar@rabbithost:5672/foo' } self.assertEquals(result, expected) @patch(open_builtin) def test_amqp_context_with_data_ssl(self, _open): '''Test amqp context with all required data and ssl''' relation = FakeRelation(relation_data=AMQP_RELATION_WITH_SSL) self.relation_get.side_effect = relation.get self.config.return_value = AMQP_CONFIG ssl_dir = '/etc/sslamqp' amqp = context.AMQPContext(ssl_dir=ssl_dir) result = amqp() expected = { 'oslo_messaging_driver': 'messagingv2', 'rabbitmq_host': 'rabbithost', 'rabbitmq_password': 'foobar', 'rabbitmq_user': 'adam', 'rabbit_ssl_port': 5671, 'rabbitmq_virtual_host': 'foo', 'rabbit_ssl_ca': ssl_dir + '/rabbit-client-ca.pem', 'rabbitmq_ha_queues': True, 'transport_url': 'rabbit://adam:foobar@rabbithost:5671/foo' } _open.assert_called_once_with(ssl_dir + '/rabbit-client-ca.pem', 'wb') self.assertEquals(result, expected) self.assertEquals([call(AMQP_RELATION_WITH_SSL['ssl_ca'])], self.b64decode.call_args_list) def test_amqp_context_with_data_ssl_noca(self): '''Test amqp context with all required data with ssl but missing ca''' relation = FakeRelation(relation_data=AMQP_RELATION_WITH_SSL) self.relation_get.side_effect = relation.get self.config.return_value = AMQP_CONFIG amqp = context.AMQPContext() result = amqp() expected = { 'oslo_messaging_driver': 'messagingv2', 'rabbitmq_host': 'rabbithost', 'rabbitmq_password': 'foobar', 'rabbitmq_user': 'adam', 'rabbit_ssl_port': 5671, 'rabbitmq_virtual_host': 'foo', 'rabbit_ssl_ca': 'cert', 'rabbitmq_ha_queues': True, 'transport_url': 'rabbit://adam:foobar@rabbithost:5671/foo' } self.assertEquals(result, expected) def test_amqp_context_with_data_clustered(self): '''Test amqp context with all required data with clustered rabbit''' relation_data = copy.copy(AMQP_RELATION) relation_data['clustered'] = 'yes' relation = FakeRelation(relation_data=relation_data) self.relation_get.side_effect = relation.get self.config.return_value = AMQP_CONFIG amqp = context.AMQPContext() result = amqp() expected = { 'oslo_messaging_driver': 'messagingv2', 'clustered': True, 'rabbitmq_host': relation_data['vip'], 'rabbitmq_password': 'foobar', 'rabbitmq_user': 'adam', 'rabbitmq_virtual_host': 'foo', 'transport_url': 'rabbit://adam:foobar@10.0.0.1:5672/foo' } self.assertEquals(result, expected) def test_amqp_context_with_data_active_active(self): '''Test amqp context with required data with active/active rabbit''' relation_data = copy.copy(AMQP_AA_RELATION) relation = FakeRelation(relation_data=relation_data) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units self.config.return_value = AMQP_CONFIG amqp = context.AMQPContext() result = amqp() expected = { 'oslo_messaging_driver': 'messagingv2', 'rabbitmq_host': 'rabbithost1', 'rabbitmq_password': 'foobar', 'rabbitmq_user': 'adam', 'rabbitmq_virtual_host': 'foo', 'rabbitmq_hosts': 'rabbithost1,rabbithost2', 'transport_url': ('rabbit://adam:foobar@rabbithost1:5672' ',adam:foobar@rabbithost2:5672/foo') } self.assertEquals(result, expected) def test_amqp_context_with_missing_relation(self): '''Test amqp context missing relation data''' incomplete_relation = copy.copy(AMQP_RELATION) incomplete_relation['password'] = '' relation = FakeRelation(relation_data=incomplete_relation) self.relation_get.side_effect = relation.get self.config.return_value = AMQP_CONFIG amqp = context.AMQPContext() result = amqp() self.assertEquals({}, result) def test_amqp_context_with_missing_config(self): '''Test amqp context missing relation data''' incomplete_config = copy.copy(AMQP_CONFIG) del incomplete_config['rabbit-user'] relation = FakeRelation(relation_data=AMQP_RELATION) self.relation_get.side_effect = relation.get self.config.return_value = incomplete_config amqp = context.AMQPContext() self.assertRaises(context.OSContextError, amqp) @patch('charmhelpers.contrib.openstack.context.format_ipv6_addr') def test_amqp_context_with_ipv6(self, format_ipv6_addr): '''Test amqp context with ipv6''' relation_data = copy.copy(AMQP_AA_RELATION) relation = FakeRelation(relation_data=relation_data) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units format_ipv6_addr.return_value = '[2001:db8:1::1]' self.config.return_value = AMQP_CONFIG amqp = context.AMQPContext() result = amqp() expected = { 'oslo_messaging_driver': 'messagingv2', 'rabbitmq_host': '[2001:db8:1::1]', 'rabbitmq_password': 'foobar', 'rabbitmq_user': 'adam', 'rabbitmq_virtual_host': 'foo', 'rabbitmq_hosts': '[2001:db8:1::1],[2001:db8:1::1]', 'transport_url': ('rabbit://adam:foobar@[2001:db8:1::1]:5672' ',adam:foobar@[2001:db8:1::1]:5672/foo') } self.assertEquals(result, expected) def test_amqp_context_with_oslo_messaging(self): """Test amqp context with oslo-messaging-flags option""" relation = FakeRelation(relation_data=AMQP_RELATION) self.relation_get.side_effect = relation.get AMQP_OSLO_CONFIG.update(AMQP_CONFIG) self.config.return_value = AMQP_OSLO_CONFIG amqp = context.AMQPContext() result = amqp() expected = { 'rabbitmq_host': 'rabbithost', 'rabbitmq_password': 'foobar', 'rabbitmq_user': 'adam', 'rabbitmq_virtual_host': 'foo', 'oslo_messaging_flags': { 'rabbit_max_retries': '1', 'rabbit_retry_backoff': '1', 'rabbit_retry_interval': '1' }, 'oslo_messaging_driver': 'log', 'transport_url': 'rabbit://adam:foobar@rabbithost:5672/foo' } self.assertEquals(result, expected) def test_amqp_context_with_notification_format(self): """Test amqp context with notification_format option""" relation = FakeRelation(relation_data=AMQP_RELATION) self.relation_get.side_effect = relation.get AMQP_NOTIFICATION_FORMAT.update(AMQP_CONFIG) self.config.return_value = AMQP_NOTIFICATION_FORMAT amqp = context.AMQPContext() result = amqp() expected = { 'oslo_messaging_driver': 'messagingv2', 'rabbitmq_host': 'rabbithost', 'rabbitmq_password': 'foobar', 'rabbitmq_user': 'adam', 'rabbitmq_virtual_host': 'foo', 'notification_format': 'both', 'transport_url': 'rabbit://adam:foobar@rabbithost:5672/foo' } self.assertEquals(result, expected) def test_amqp_context_with_notification_topics(self): """Test amqp context with notification_topics option""" relation = FakeRelation(relation_data=AMQP_RELATION) self.relation_get.side_effect = relation.get AMQP_NOTIFICATION_TOPICS.update(AMQP_CONFIG) self.config.return_value = AMQP_NOTIFICATION_TOPICS amqp = context.AMQPContext() result = amqp() expected = { 'oslo_messaging_driver': 'messagingv2', 'rabbitmq_host': 'rabbithost', 'rabbitmq_password': 'foobar', 'rabbitmq_user': 'adam', 'rabbitmq_virtual_host': 'foo', 'notification_topics': 'foo,bar', 'transport_url': 'rabbit://adam:foobar@rabbithost:5672/foo' } self.assertEquals(result, expected) def test_amqp_context_with_notifications_to_logs(self): """Test amqp context with send_notifications_to_logs""" relation = FakeRelation(relation_data=AMQP_RELATION) self.relation_get.side_effect = relation.get AMQP_NOTIFICATIONS_LOGS.update(AMQP_CONFIG) self.config.return_value = AMQP_NOTIFICATIONS_LOGS amqp = context.AMQPContext() result = amqp() expected = { 'oslo_messaging_driver': 'messagingv2', 'rabbitmq_host': 'rabbithost', 'rabbitmq_password': 'foobar', 'rabbitmq_user': 'adam', 'rabbitmq_virtual_host': 'foo', 'transport_url': 'rabbit://adam:foobar@rabbithost:5672/foo', 'send_notifications_to_logs': True, } self.assertEquals(result, expected) def test_libvirt_config_flags(self): self.config.side_effect = fake_config({ 'libvirt-flags': 'iscsi_use_multipath=True,chap_auth=False', }) results = context.LibvirtConfigFlagsContext()() self.assertEquals(results, { 'libvirt_flags': { 'chap_auth': 'False', 'iscsi_use_multipath': 'True' } }) def test_ceph_no_relids(self): '''Test empty ceph realtion''' relation = FakeRelation(relation_data={}) self.relation_ids.side_effect = relation.get ceph = context.CephContext() result = ceph() self.assertEquals(result, {}) def test_ceph_rel_with_no_units(self): '''Test ceph context with missing related units''' relation = FakeRelation(relation_data={}) self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = [] ceph = context.CephContext() result = ceph() self.assertEquals(result, {}) @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_with_data(self, ensure_packages, mkdir, isdir, mock_config): '''Test ceph context with all relation data''' config_dict = {'use-syslog': True} def fake_config(key): return config_dict.get(key) mock_config.side_effect = fake_config isdir.return_value = False relation = FakeRelation(relation_data=CEPH_RELATION) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() expected = { 'mon_hosts': 'ceph_node1 ceph_node2', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', } self.assertEquals(result, expected) ensure_packages.assert_called_with(['ceph-common']) @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_with_missing_data(self, ensure_packages, mkdir): '''Test ceph context with missing relation data''' relation = copy.deepcopy(CEPH_RELATION) for k, v in six.iteritems(relation): for u in six.iterkeys(v): del relation[k][u]['auth'] relation = FakeRelation(relation_data=relation) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() self.assertEquals(result, {}) self.assertFalse(ensure_packages.called) @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_partial_missing_data(self, ensure_packages, mkdir, isdir, config): '''Test ceph context last unit missing data Tests a fix to a previously bug which meant only the config from last unit was returned so if a valid value was supplied from an earlier unit it would be ignored''' config.side_effect = fake_config({'use-syslog': 'True'}) relation = copy.deepcopy(CEPH_RELATION) for k, v in six.iteritems(relation): last_unit = sorted(six.iterkeys(v))[-1] unit_data = relation[k][last_unit] del unit_data['auth'] relation[k][last_unit] = unit_data relation = FakeRelation(relation_data=relation) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() expected = { 'mon_hosts': 'ceph_node1 ceph_node2', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', } self.assertEquals(result, expected) @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_with_public_addr( self, ensure_packages, mkdir, isdir, mock_config): '''Test ceph context in host with multiple networks with all relation data''' isdir.return_value = False config_dict = {'use-syslog': True} def fake_config(key): return config_dict.get(key) mock_config.side_effect = fake_config relation = FakeRelation(relation_data=CEPH_RELATION_WITH_PUBLIC_ADDR) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() expected = { 'mon_hosts': '192.168.1.10 192.168.1.11', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', } self.assertEquals(result, expected) ensure_packages.assert_called_with(['ceph-common']) mkdir.assert_called_with('/etc/ceph') @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_with_public_addr_and_port( self, ensure_packages, mkdir, isdir, mock_config): '''Test ceph context in host with multiple networks with all relation data''' isdir.return_value = False config_dict = {'use-syslog': True} def fake_config(key): return config_dict.get(key) mock_config.side_effect = fake_config relation = FakeRelation(relation_data=CEPH_REL_WITH_PUBLIC_ADDR_PORT) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() expected = { 'mon_hosts': '192.168.1.10:1234 192.168.1.11:4321', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', } self.assertEquals(result, expected) ensure_packages.assert_called_with(['ceph-common']) mkdir.assert_called_with('/etc/ceph') @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_with_public_ipv6_addr(self, ensure_packages, mkdir, isdir, mock_config): '''Test ceph context in host with multiple networks with all relation data''' isdir.return_value = False config_dict = {'use-syslog': True} def fake_config(key): return config_dict.get(key) mock_config.side_effect = fake_config relation = FakeRelation(relation_data=CEPH_REL_WITH_PUBLIC_IPv6_ADDR) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() expected = { 'mon_hosts': '[2001:5c0:9168::1] [2001:5c0:9168::2]', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', } self.assertEquals(result, expected) ensure_packages.assert_called_with(['ceph-common']) mkdir.assert_called_with('/etc/ceph') @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_with_public_ipv6_addr_port( self, ensure_packages, mkdir, isdir, mock_config): '''Test ceph context in host with multiple networks with all relation data''' isdir.return_value = False config_dict = {'use-syslog': True} def fake_config(key): return config_dict.get(key) mock_config.side_effect = fake_config relation = FakeRelation( relation_data=CEPH_REL_WITH_PUBLIC_IPv6_ADDR_PORT) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() expected = { 'mon_hosts': '[2001:5c0:9168::1]:1234 [2001:5c0:9168::2]:4321', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', } self.assertEquals(result, expected) ensure_packages.assert_called_with(['ceph-common']) mkdir.assert_called_with('/etc/ceph') @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_with_multi_public_addr( self, ensure_packages, mkdir, isdir, mock_config): '''Test ceph context in host with multiple networks with all relation data''' isdir.return_value = False config_dict = {'use-syslog': True} def fake_config(key): return config_dict.get(key) mock_config.side_effect = fake_config relation = FakeRelation(relation_data=CEPH_REL_WITH_MULTI_PUBLIC_ADDR) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() expected = { 'mon_hosts': '192.168.1.10 192.168.1.11 192.168.1.20 192.168.1.21', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', } self.assertEquals(result, expected) ensure_packages.assert_called_with(['ceph-common']) mkdir.assert_called_with('/etc/ceph') @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_with_default_features( self, ensure_packages, mkdir, isdir, mock_config): '''Test ceph context in host with multiple networks with all relation data''' isdir.return_value = False config_dict = {'use-syslog': True} def fake_config(key): return config_dict.get(key) mock_config.side_effect = fake_config relation = FakeRelation(relation_data=CEPH_REL_WITH_DEFAULT_FEATURES) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() expected = { 'mon_hosts': 'ceph_node1 ceph_node2', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', 'rbd_features': '1', } self.assertEquals(result, expected) ensure_packages.assert_called_with(['ceph-common']) mkdir.assert_called_with('/etc/ceph') @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_ec_pool_no_rbd_pool( self, ensure_packages, mkdir, isdir, mock_config): '''Test ceph context with erasure coded pools''' isdir.return_value = False config_dict = { 'use-syslog': True, 'pool-type': 'erasure-coded' } def fake_config(key): return config_dict.get(key) mock_config.side_effect = fake_config relation = FakeRelation(relation_data=CEPH_REL_WITH_DEFAULT_FEATURES) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() expected = { 'mon_hosts': 'ceph_node1 ceph_node2', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', 'rbd_features': '1', 'rbd_default_data_pool': 'testing-foo', } self.assertEquals(result, expected) ensure_packages.assert_called_with(['ceph-common']) mkdir.assert_called_with('/etc/ceph') @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_ec_pool_rbd_pool( self, ensure_packages, mkdir, isdir, mock_config): '''Test ceph context with erasure coded pools''' isdir.return_value = False config_dict = { 'use-syslog': True, 'pool-type': 'erasure-coded', 'rbd-pool': 'glance' } def fake_config(key): return config_dict.get(key) mock_config.side_effect = fake_config relation = FakeRelation(relation_data=CEPH_REL_WITH_DEFAULT_FEATURES) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() expected = { 'mon_hosts': 'ceph_node1 ceph_node2', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', 'rbd_features': '1', 'rbd_default_data_pool': 'glance', } self.assertEquals(result, expected) ensure_packages.assert_called_with(['ceph-common']) mkdir.assert_called_with('/etc/ceph') @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_ec_pool_rbd_pool_name( self, ensure_packages, mkdir, isdir, mock_config): '''Test ceph context with erasure coded pools''' isdir.return_value = False config_dict = { 'use-syslog': True, 'pool-type': 'erasure-coded', 'rbd-pool-name': 'nova' } def fake_config(key): return config_dict.get(key) mock_config.side_effect = fake_config relation = FakeRelation(relation_data=CEPH_REL_WITH_DEFAULT_FEATURES) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() expected = { 'mon_hosts': 'ceph_node1 ceph_node2', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', 'rbd_features': '1', 'rbd_default_data_pool': 'nova', } self.assertEquals(result, expected) ensure_packages.assert_called_with(['ceph-common']) mkdir.assert_called_with('/etc/ceph') @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_with_rbd_cache(self, ensure_packages, mkdir, isdir, mock_config): isdir.return_value = False config_dict = {'rbd-client-cache': 'enabled', 'use-syslog': False} def fake_config(key): return config_dict.get(key) mock_config.side_effect = fake_config relation = FakeRelation(relation_data=CEPH_RELATION_WITH_PUBLIC_ADDR) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units class CephContextWithRBDCache(context.CephContext): def __call__(self): ctxt = super(CephContextWithRBDCache, self).__call__() rbd_cache = fake_config('rbd-client-cache') or "" if rbd_cache.lower() == "enabled": ctxt['rbd_client_cache_settings'] = \ {'rbd cache': 'true', 'rbd cache writethrough until flush': 'true'} elif rbd_cache.lower() == "disabled": ctxt['rbd_client_cache_settings'] = \ {'rbd cache': 'false'} return ctxt ceph = CephContextWithRBDCache() result = ceph() expected = { 'mon_hosts': '192.168.1.10 192.168.1.11', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'false', } expected['rbd_client_cache_settings'] = \ {'rbd cache': 'true', 'rbd cache writethrough until flush': 'true'} self.assertDictEqual(result, expected) ensure_packages.assert_called_with(['ceph-common']) mkdir.assert_called_with('/etc/ceph') @patch.object(context, 'config') def test_sysctl_context_with_config(self, config): self.charm_name.return_value = 'test-charm' config.return_value = '{ kernel.max_pid: "1337"}' self.sysctl_create.return_value = True ctxt = context.SysctlContext() result = ctxt() self.sysctl_create.assert_called_with( config.return_value, "/etc/sysctl.d/50-test-charm.conf") self.assertTrue(result, {'sysctl': config.return_value}) @patch.object(context, 'config') def test_sysctl_context_without_config(self, config): self.charm_name.return_value = 'test-charm' config.return_value = None self.sysctl_create.return_value = True ctxt = context.SysctlContext() result = ctxt() self.assertTrue(self.sysctl_create.called == 0) self.assertTrue(result, {'sysctl': config.return_value}) @patch.object(context, 'config') @patch('os.path.isdir') @patch('os.mkdir') @patch.object(context, 'ensure_packages') def test_ceph_context_missing_public_addr( self, ensure_packages, mkdir, isdir, mock_config): '''Test ceph context in host with multiple networks with no ceph-public-addr in relation data''' isdir.return_value = False config_dict = {'use-syslog': True} def fake_config(key): return config_dict.get(key) mock_config.side_effect = fake_config relation = copy.deepcopy(CEPH_RELATION_WITH_PUBLIC_ADDR) del relation['ceph:0']['ceph/0']['ceph-public-address'] relation = FakeRelation(relation_data=relation) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units ceph = context.CephContext() result = ceph() expected = { 'mon_hosts': '192.168.1.11 ceph_node1', 'auth': 'foo', 'key': 'bar', 'use_syslog': 'true', } self.assertEquals(result, expected) ensure_packages.assert_called_with(['ceph-common']) mkdir.assert_called_with('/etc/ceph') @patch('charmhelpers.contrib.openstack.context.local_address') @patch('charmhelpers.contrib.openstack.context.local_unit') def test_haproxy_context_with_data(self, local_unit, local_address): '''Test haproxy context with all relation data''' cluster_relation = { 'cluster:0': { 'peer/1': { 'private-address': 'cluster-peer1.localnet', }, 'peer/2': { 'private-address': 'cluster-peer2.localnet', }, }, } local_unit.return_value = 'peer/0' # We are only using get_relation_ip. # Setup the values it returns on each subsequent call. self.get_relation_ip.side_effect = [None, None, None, 'cluster-peer0.localnet'] relation = FakeRelation(cluster_relation) self.relation_ids.side_effect = relation.relation_ids self.relation_get.side_effect = relation.get self.related_units.side_effect = relation.relation_units self.get_netmask_for_address.return_value = '255.255.0.0' self.config.return_value = False self.maxDiff = None self.is_ipv6_disabled.return_value = True haproxy = context.HAProxyContext() with patch_open() as (_open, _file): result = haproxy() ex = { 'frontends': { 'cluster-peer0.localnet': { 'network': 'cluster-peer0.localnet/255.255.0.0', 'backends': collections.OrderedDict([ ('peer-0', 'cluster-peer0.localnet'), ('peer-1', 'cluster-peer1.localnet'), ('peer-2', 'cluster-peer2.localnet'), ]), }, }, 'default_backend': 'cluster-peer0.localnet', 'local_host': '127.0.0.1', 'haproxy_host': '0.0.0.0', 'ipv6_enabled': False, 'stat_password': 'testpassword', 'stat_port': '8888', } # the context gets generated. self.assertEquals(ex, result) # and /etc/default/haproxy is updated. self.assertEquals(_file.write.call_args_list, [call('ENABLED=1\n')]) self.get_relation_ip.assert_has_calls([call('admin', False), call('internal', False), call('public', False), call('cluster')]) @patch('charmhelpers.contrib.openstack.context.local_address') @patch('charmhelpers.contrib.openstack.context.local_unit') def test_haproxy_context_with_data_timeout(self, local_unit, local_address): '''Test haproxy context with all relation data and timeout''' cluster_relation = { 'cluster:0': { 'peer/1': { 'private-address': 'cluster-peer1.localnet', }, 'peer/2': { 'private-address': 'cluster-peer2.localnet', }, }, } local_unit.return_value = 'peer/0' # We are only using get_relation_ip. # Setup the values it returns on each subsequent call. self.get_relation_ip.side_effect = [None, None, None, 'cluster-peer0.localnet'] relation = FakeRelation(cluster_relation) self.relation_ids.side_effect = relation.relation_ids self.relation_get.side_effect = relation.get self.related_units.side_effect = relation.relation_units self.get_netmask_for_address.return_value = '255.255.0.0' self.config.return_value = False self.maxDiff = None c = fake_config(HAPROXY_CONFIG) c.data['prefer-ipv6'] = False self.config.side_effect = c self.is_ipv6_disabled.return_value = True haproxy = context.HAProxyContext() with patch_open() as (_open, _file): result = haproxy() ex = { 'frontends': { 'cluster-peer0.localnet': { 'network': 'cluster-peer0.localnet/255.255.0.0', 'backends': collections.OrderedDict([ ('peer-0', 'cluster-peer0.localnet'), ('peer-1', 'cluster-peer1.localnet'), ('peer-2', 'cluster-peer2.localnet'), ]), } }, 'default_backend': 'cluster-peer0.localnet', 'local_host': '127.0.0.1', 'haproxy_host': '0.0.0.0', 'ipv6_enabled': False, 'stat_password': 'testpassword', 'stat_port': '8888', 'haproxy_client_timeout': 50000, 'haproxy_server_timeout': 50000, } # the context gets generated. self.assertEquals(ex, result) # and /etc/default/haproxy is updated. self.assertEquals(_file.write.call_args_list, [call('ENABLED=1\n')]) self.get_relation_ip.assert_has_calls([call('admin', None), call('internal', None), call('public', None), call('cluster')]) @patch('charmhelpers.contrib.openstack.context.local_address') @patch('charmhelpers.contrib.openstack.context.local_unit') def test_haproxy_context_with_data_multinet(self, local_unit, local_address): '''Test haproxy context with all relation data for network splits''' cluster_relation = { 'cluster:0': { 'peer/1': { 'private-address': 'cluster-peer1.localnet', 'admin-address': 'cluster-peer1.admin', 'internal-address': 'cluster-peer1.internal', 'public-address': 'cluster-peer1.public', }, 'peer/2': { 'private-address': 'cluster-peer2.localnet', 'admin-address': 'cluster-peer2.admin', 'internal-address': 'cluster-peer2.internal', 'public-address': 'cluster-peer2.public', }, }, } local_unit.return_value = 'peer/0' relation = FakeRelation(cluster_relation) self.relation_ids.side_effect = relation.relation_ids self.relation_get.side_effect = relation.get self.related_units.side_effect = relation.relation_units # We are only using get_relation_ip. # Setup the values it returns on each subsequent call. self.get_relation_ip.side_effect = ['cluster-peer0.admin', 'cluster-peer0.internal', 'cluster-peer0.public', 'cluster-peer0.localnet'] self.get_netmask_for_address.return_value = '255.255.0.0' self.config.return_value = False self.maxDiff = None self.is_ipv6_disabled.return_value = True haproxy = context.HAProxyContext() with patch_open() as (_open, _file): result = haproxy() ex = { 'frontends': { 'cluster-peer0.admin': { 'network': 'cluster-peer0.admin/255.255.0.0', 'backends': collections.OrderedDict([ ('peer-0', 'cluster-peer0.admin'), ('peer-1', 'cluster-peer1.admin'), ('peer-2', 'cluster-peer2.admin'), ]), }, 'cluster-peer0.internal': { 'network': 'cluster-peer0.internal/255.255.0.0', 'backends': collections.OrderedDict([ ('peer-0', 'cluster-peer0.internal'), ('peer-1', 'cluster-peer1.internal'), ('peer-2', 'cluster-peer2.internal'), ]), }, 'cluster-peer0.public': { 'network': 'cluster-peer0.public/255.255.0.0', 'backends': collections.OrderedDict([ ('peer-0', 'cluster-peer0.public'), ('peer-1', 'cluster-peer1.public'), ('peer-2', 'cluster-peer2.public'), ]), }, 'cluster-peer0.localnet': { 'network': 'cluster-peer0.localnet/255.255.0.0', 'backends': collections.OrderedDict([ ('peer-0', 'cluster-peer0.localnet'), ('peer-1', 'cluster-peer1.localnet'), ('peer-2', 'cluster-peer2.localnet'), ]), } }, 'default_backend': 'cluster-peer0.localnet', 'local_host': '127.0.0.1', 'haproxy_host': '0.0.0.0', 'ipv6_enabled': False, 'stat_password': 'testpassword', 'stat_port': '8888', } # the context gets generated. self.assertEquals(ex, result) # and /etc/default/haproxy is updated. self.assertEquals(_file.write.call_args_list, [call('ENABLED=1\n')]) self.get_relation_ip.assert_has_calls([call('admin', False), call('internal', False), call('public', False), call('cluster')]) @patch('charmhelpers.contrib.openstack.context.local_address') @patch('charmhelpers.contrib.openstack.context.local_unit') def test_haproxy_context_with_data_public_only(self, local_unit, local_address): '''Test haproxy context with with openstack-dashboard public only binding''' cluster_relation = { 'cluster:0': { 'peer/1': { 'private-address': 'cluster-peer1.localnet', 'public-address': 'cluster-peer1.public', }, 'peer/2': { 'private-address': 'cluster-peer2.localnet', 'public-address': 'cluster-peer2.public', }, }, } local_unit.return_value = 'peer/0' relation = FakeRelation(cluster_relation) self.relation_ids.side_effect = relation.relation_ids self.relation_get.side_effect = relation.get self.related_units.side_effect = relation.relation_units # We are only using get_relation_ip. # Setup the values it returns on each subsequent call. _network_get_map = { 'public': 'cluster-peer0.public', 'cluster': 'cluster-peer0.localnet', } self.get_relation_ip.side_effect = ( lambda binding, config_opt=None: _network_get_map[binding] ) self.get_netmask_for_address.return_value = '255.255.0.0' self.config.return_value = None self.maxDiff = None self.is_ipv6_disabled.return_value = True haproxy = context.HAProxyContext(address_types=['public']) with patch_open() as (_open, _file): result = haproxy() ex = { 'frontends': { 'cluster-peer0.public': { 'network': 'cluster-peer0.public/255.255.0.0', 'backends': collections.OrderedDict([ ('peer-0', 'cluster-peer0.public'), ('peer-1', 'cluster-peer1.public'), ('peer-2', 'cluster-peer2.public'), ]), }, 'cluster-peer0.localnet': { 'network': 'cluster-peer0.localnet/255.255.0.0', 'backends': collections.OrderedDict([ ('peer-0', 'cluster-peer0.localnet'), ('peer-1', 'cluster-peer1.localnet'), ('peer-2', 'cluster-peer2.localnet'), ]), } }, 'default_backend': 'cluster-peer0.localnet', 'local_host': '127.0.0.1', 'haproxy_host': '0.0.0.0', 'ipv6_enabled': False, 'stat_password': 'testpassword', 'stat_port': '8888', } # the context gets generated. self.assertEquals(ex, result) # and /etc/default/haproxy is updated. self.assertEquals(_file.write.call_args_list, [call('ENABLED=1\n')]) self.get_relation_ip.assert_has_calls([call('public', None), call('cluster')]) @patch('charmhelpers.contrib.openstack.context.local_address') @patch('charmhelpers.contrib.openstack.context.local_unit') def test_haproxy_context_with_data_ipv6(self, local_unit, local_address): '''Test haproxy context with all relation data ipv6''' cluster_relation = { 'cluster:0': { 'peer/1': { 'private-address': 'cluster-peer1.localnet', }, 'peer/2': { 'private-address': 'cluster-peer2.localnet', }, }, } local_unit.return_value = 'peer/0' # We are only using get_relation_ip. # Setup the values it returns on each subsequent call. self.get_relation_ip.side_effect = [None, None, None, 'cluster-peer0.localnet'] relation = FakeRelation(cluster_relation) self.relation_ids.side_effect = relation.relation_ids self.relation_get.side_effect = relation.get self.related_units.side_effect = relation.relation_units self.get_address_in_network.return_value = None self.get_netmask_for_address.return_value = \ 'FFFF:FFFF:FFFF:FFFF:0000:0000:0000:0000' self.get_ipv6_addr.return_value = ['cluster-peer0.localnet'] c = fake_config(HAPROXY_CONFIG) c.data['prefer-ipv6'] = True self.config.side_effect = c self.maxDiff = None self.is_ipv6_disabled.return_value = False haproxy = context.HAProxyContext() with patch_open() as (_open, _file): result = haproxy() ex = { 'frontends': { 'cluster-peer0.localnet': { 'network': 'cluster-peer0.localnet/' 'FFFF:FFFF:FFFF:FFFF:0000:0000:0000:0000', 'backends': collections.OrderedDict([ ('peer-0', 'cluster-peer0.localnet'), ('peer-1', 'cluster-peer1.localnet'), ('peer-2', 'cluster-peer2.localnet'), ]), } }, 'default_backend': 'cluster-peer0.localnet', 'local_host': 'ip6-localhost', 'haproxy_server_timeout': 50000, 'haproxy_client_timeout': 50000, 'haproxy_host': '::', 'ipv6_enabled': True, 'stat_password': 'testpassword', 'stat_port': '8888', } # the context gets generated. self.assertEquals(ex, result) # and /etc/default/haproxy is updated. self.assertEquals(_file.write.call_args_list, [call('ENABLED=1\n')]) self.get_relation_ip.assert_has_calls([call('admin', None), call('internal', None), call('public', None), call('cluster')]) def test_haproxy_context_with_missing_data(self): '''Test haproxy context with missing relation data''' self.relation_ids.return_value = [] haproxy = context.HAProxyContext() self.assertEquals({}, haproxy()) @patch('charmhelpers.contrib.openstack.context.local_address') @patch('charmhelpers.contrib.openstack.context.local_unit') def test_haproxy_context_with_no_peers(self, local_unit, local_address): '''Test haproxy context with single unit''' # peer relations always show at least one peer relation, even # if unit is alone. should be an incomplete context. cluster_relation = { 'cluster:0': { 'peer/0': { 'private-address': 'lonely.clusterpeer.howsad', }, }, } local_unit.return_value = 'peer/0' # We are only using get_relation_ip. # Setup the values it returns on each subsequent call. self.get_relation_ip.side_effect = [None, None, None, None] relation = FakeRelation(cluster_relation) self.relation_ids.side_effect = relation.relation_ids self.relation_get.side_effect = relation.get self.related_units.side_effect = relation.relation_units self.config.return_value = False haproxy = context.HAProxyContext() self.assertEquals({}, haproxy()) self.get_relation_ip.assert_has_calls([call('admin', False), call('internal', False), call('public', False), call('cluster')]) @patch('charmhelpers.contrib.openstack.context.local_address') @patch('charmhelpers.contrib.openstack.context.local_unit') def test_haproxy_context_with_net_override(self, local_unit, local_address): '''Test haproxy context with single unit''' # peer relations always show at least one peer relation, even # if unit is alone. should be an incomplete context. cluster_relation = { 'cluster:0': { 'peer/0': { 'private-address': 'lonely.clusterpeer.howsad', }, }, } local_unit.return_value = 'peer/0' # We are only using get_relation_ip. # Setup the values it returns on each subsequent call. self.get_relation_ip.side_effect = [None, None, None, None] relation = FakeRelation(cluster_relation) self.relation_ids.side_effect = relation.relation_ids self.relation_get.side_effect = relation.get self.related_units.side_effect = relation.relation_units self.config.return_value = False c = fake_config(HAPROXY_CONFIG) c.data['os-admin-network'] = '192.168.10.0/24' c.data['os-internal-network'] = '192.168.20.0/24' c.data['os-public-network'] = '192.168.30.0/24' self.config.side_effect = c haproxy = context.HAProxyContext() self.assertEquals({}, haproxy()) self.get_relation_ip.assert_has_calls([call('admin', '192.168.10.0/24'), call('internal', '192.168.20.0/24'), call('public', '192.168.30.0/24'), call('cluster')]) @patch('charmhelpers.contrib.openstack.context.local_address') @patch('charmhelpers.contrib.openstack.context.local_unit') def test_haproxy_context_with_no_peers_singlemode(self, local_unit, local_address): '''Test haproxy context with single unit''' # peer relations always show at least one peer relation, even # if unit is alone. should be an incomplete context. cluster_relation = { 'cluster:0': { 'peer/0': { 'private-address': 'lonely.clusterpeer.howsad', }, }, } local_unit.return_value = 'peer/0' # We are only using get_relation_ip. # Setup the values it returns on each subsequent call. self.get_relation_ip.side_effect = [None, None, None, 'lonely.clusterpeer.howsad'] relation = FakeRelation(cluster_relation) self.relation_ids.side_effect = relation.relation_ids self.relation_get.side_effect = relation.get self.related_units.side_effect = relation.relation_units self.config.return_value = False self.get_address_in_network.return_value = None self.get_netmask_for_address.return_value = '255.255.0.0' self.is_ipv6_disabled.return_value = True with patch_open() as (_open, _file): result = context.HAProxyContext(singlenode_mode=True)() ex = { 'frontends': { 'lonely.clusterpeer.howsad': { 'backends': collections.OrderedDict([ ('peer-0', 'lonely.clusterpeer.howsad')]), 'network': 'lonely.clusterpeer.howsad/255.255.0.0' }, }, 'default_backend': 'lonely.clusterpeer.howsad', 'haproxy_host': '0.0.0.0', 'local_host': '127.0.0.1', 'ipv6_enabled': False, 'stat_port': '8888', 'stat_password': 'testpassword', } self.assertEquals(ex, result) # and /etc/default/haproxy is updated. self.assertEquals(_file.write.call_args_list, [call('ENABLED=1\n')]) self.get_relation_ip.assert_has_calls([call('admin', False), call('internal', False), call('public', False), call('cluster')]) def test_https_context_with_no_https(self): '''Test apache2 https when no https data available''' apache = context.ApacheSSLContext() self.https.return_value = False self.assertEquals({}, apache()) def _https_context_setup(self): ''' Helper for test_https_context* tests. ''' self.https.return_value = True self.determine_api_port.return_value = 8756 self.determine_apache_port.return_value = 8766 apache = context.ApacheSSLContext() apache.configure_cert = MagicMock() apache.enable_modules = MagicMock() apache.configure_ca = MagicMock() apache.canonical_names = MagicMock() apache.canonical_names.return_value = [ '10.5.1.1', '10.5.2.1', '10.5.3.1', ] apache.get_network_addresses = MagicMock() apache.get_network_addresses.return_value = [ ('10.5.1.100', '10.5.1.1'), ('10.5.2.100', '10.5.2.1'), ('10.5.3.100', '10.5.3.1'), ] apache.external_ports = '8776' apache.service_namespace = 'cinder' ex = { 'namespace': 'cinder', 'endpoints': [('10.5.1.100', '10.5.1.1', 8766, 8756), ('10.5.2.100', '10.5.2.1', 8766, 8756), ('10.5.3.100', '10.5.3.1', 8766, 8756)], 'ext_ports': [8766] } return apache, ex def test_https_context(self): self.relation_ids.return_value = [] apache, ex = self._https_context_setup() self.assertEquals(ex, apache()) apache.configure_cert.assert_has_calls([ call('10.5.1.1'), call('10.5.2.1'), call('10.5.3.1') ]) self.assertTrue(apache.configure_ca.called) self.assertTrue(apache.enable_modules.called) self.assertTrue(apache.configure_cert.called) def test_https_context_vault_relation(self): self.relation_ids.return_value = ['certificates:2'] self.related_units.return_value = 'vault/0' apache, ex = self._https_context_setup() self.assertEquals(ex, apache()) self.assertFalse(apache.configure_cert.called) self.assertFalse(apache.configure_ca.called) def test_https_context_no_canonical_names(self): self.relation_ids.return_value = [] apache, ex = self._https_context_setup() apache.canonical_names.return_value = [] self.resolve_address.side_effect = ( '10.5.1.4', '10.5.2.5', '10.5.3.6') self.assertEquals(ex, apache()) apache.configure_cert.assert_has_calls([ call('10.5.1.4'), call('10.5.2.5'), call('10.5.3.6') ]) self.resolve_address.assert_has_calls([ call(endpoint_type=context.INTERNAL), call(endpoint_type=context.ADMIN), call(endpoint_type=context.PUBLIC), ]) self.assertTrue(apache.configure_ca.called) self.assertTrue(apache.enable_modules.called) self.assertTrue(apache.configure_cert.called) def test_https_context_loads_correct_apache_mods(self): # Test apache2 context also loads required apache modules apache = context.ApacheSSLContext() apache.enable_modules() ex_cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http', 'headers'] self.check_call.assert_called_with(ex_cmd) def test_https_configure_cert(self): # Test apache2 properly installs certs and keys to disk self.get_cert.return_value = ('SSL_CERT', 'SSL_KEY') self.b64decode.side_effect = [b'SSL_CERT', b'SSL_KEY'] apache = context.ApacheSSLContext() apache.service_namespace = 'cinder' apache.configure_cert('test-cn') # appropriate directories are created. self.mkdir.assert_called_with(path='/etc/apache2/ssl/cinder') # appropriate files are written. files = [call(path='/etc/apache2/ssl/cinder/cert_test-cn', content=b'SSL_CERT', owner='root', group='root', perms=0o640), call(path='/etc/apache2/ssl/cinder/key_test-cn', content=b'SSL_KEY', owner='root', group='root', perms=0o640)] self.write_file.assert_has_calls(files) # appropriate bits are b64decoded. decode = [call('SSL_CERT'), call('SSL_KEY')] self.assertEquals(decode, self.b64decode.call_args_list) def test_https_configure_cert_deprecated(self): # Test apache2 properly installs certs and keys to disk self.get_cert.return_value = ('SSL_CERT', 'SSL_KEY') self.b64decode.side_effect = ['SSL_CERT', 'SSL_KEY'] apache = context.ApacheSSLContext() apache.service_namespace = 'cinder' apache.configure_cert() # appropriate directories are created. self.mkdir.assert_called_with(path='/etc/apache2/ssl/cinder') # appropriate files are written. files = [call(path='/etc/apache2/ssl/cinder/cert', content='SSL_CERT', owner='root', group='root', perms=0o640), call(path='/etc/apache2/ssl/cinder/key', content='SSL_KEY', owner='root', group='root', perms=0o640)] self.write_file.assert_has_calls(files) # appropriate bits are b64decoded. decode = [call('SSL_CERT'), call('SSL_KEY')] self.assertEquals(decode, self.b64decode.call_args_list) def test_https_canonical_names(self): rel = FakeRelation(IDENTITY_RELATION_SINGLE_CERT) self.relation_ids.side_effect = rel.relation_ids self.related_units.side_effect = rel.relation_units self.relation_get.side_effect = rel.get apache = context.ApacheSSLContext() self.assertEquals(apache.canonical_names(), ['cinderhost1']) rel.relation_data = IDENTITY_RELATION_MULTIPLE_CERT self.assertEquals(apache.canonical_names(), sorted(['cinderhost1-adm-network', 'cinderhost1-int-network', 'cinderhost1-pub-network'])) rel.relation_data = IDENTITY_RELATION_NO_CERT self.assertEquals(apache.canonical_names(), []) def test_image_service_context_missing_data(self): '''Test image-service with missing relation and missing data''' image_service = context.ImageServiceContext() self.relation_ids.return_value = [] self.assertEquals({}, image_service()) self.relation_ids.return_value = ['image-service:0'] self.related_units.return_value = ['glance/0'] self.relation_get.return_value = None self.assertEquals({}, image_service()) def test_image_service_context_with_data(self): '''Test image-service with required data''' image_service = context.ImageServiceContext() self.relation_ids.return_value = ['image-service:0'] self.related_units.return_value = ['glance/0'] self.relation_get.return_value = 'http://glancehost:9292' self.assertEquals({'glance_api_servers': 'http://glancehost:9292'}, image_service()) @patch.object(context, 'neutron_plugin_attribute') def test_neutron_context_base_properties(self, attr): '''Test neutron context base properties''' neutron = context.NeutronContext() attr.return_value = 'quantum-plugin-package' self.assertEquals(None, neutron.plugin) self.assertEquals(None, neutron.network_manager) self.assertEquals(None, neutron.neutron_security_groups) self.assertEquals('quantum-plugin-package', neutron.packages) @patch.object(context, 'neutron_plugin_attribute') @patch.object(context, 'apt_install') @patch.object(context, 'filter_installed_packages') def test_neutron_ensure_package(self, _filter, _install, _packages): '''Test neutron context installed required packages''' _filter.return_value = ['quantum-plugin-package'] _packages.return_value = [['quantum-plugin-package']] neutron = context.NeutronContext() neutron._ensure_packages() _install.assert_called_with(['quantum-plugin-package'], fatal=True) @patch.object(context.NeutronContext, 'neutron_security_groups') @patch.object(context, 'unit_private_ip') @patch.object(context, 'neutron_plugin_attribute') def test_neutron_ovs_plugin_context(self, attr, ip, sec_groups): ip.return_value = '10.0.0.1' sec_groups.__get__ = MagicMock(return_value=True) attr.return_value = 'some.quantum.driver.class' neutron = context.NeutronContext() self.assertEquals({ 'config': 'some.quantum.driver.class', 'core_plugin': 'some.quantum.driver.class', 'neutron_plugin': 'ovs', 'neutron_security_groups': True, 'local_ip': '10.0.0.1'}, neutron.ovs_ctxt()) @patch.object(context.NeutronContext, 'neutron_security_groups') @patch.object(context, 'unit_private_ip') @patch.object(context, 'neutron_plugin_attribute') def test_neutron_nvp_plugin_context(self, attr, ip, sec_groups): ip.return_value = '10.0.0.1' sec_groups.__get__ = MagicMock(return_value=True) attr.return_value = 'some.quantum.driver.class' neutron = context.NeutronContext() self.assertEquals({ 'config': 'some.quantum.driver.class', 'core_plugin': 'some.quantum.driver.class', 'neutron_plugin': 'nvp', 'neutron_security_groups': True, 'local_ip': '10.0.0.1'}, neutron.nvp_ctxt()) @patch.object(context, 'config') @patch.object(context.NeutronContext, 'neutron_security_groups') @patch.object(context, 'unit_private_ip') @patch.object(context, 'neutron_plugin_attribute') def test_neutron_n1kv_plugin_context(self, attr, ip, sec_groups, config): ip.return_value = '10.0.0.1' sec_groups.__get__ = MagicMock(return_value=True) attr.return_value = 'some.quantum.driver.class' config.return_value = 'n1kv' neutron = context.NeutronContext() self.assertEquals({ 'core_plugin': 'some.quantum.driver.class', 'neutron_plugin': 'n1kv', 'neutron_security_groups': True, 'local_ip': '10.0.0.1', 'config': 'some.quantum.driver.class', 'vsm_ip': 'n1kv', 'vsm_username': 'n1kv', 'vsm_password': 'n1kv', 'user_config_flags': {}, 'restrict_policy_profiles': 'n1kv', }, neutron.n1kv_ctxt()) @patch.object(context.NeutronContext, 'neutron_security_groups') @patch.object(context, 'unit_private_ip') @patch.object(context, 'neutron_plugin_attribute') def test_neutron_calico_plugin_context(self, attr, ip, sec_groups): ip.return_value = '10.0.0.1' sec_groups.__get__ = MagicMock(return_value=True) attr.return_value = 'some.quantum.driver.class' neutron = context.NeutronContext() self.assertEquals({ 'config': 'some.quantum.driver.class', 'core_plugin': 'some.quantum.driver.class', 'neutron_plugin': 'Calico', 'neutron_security_groups': True, 'local_ip': '10.0.0.1'}, neutron.calico_ctxt()) @patch.object(context.NeutronContext, 'neutron_security_groups') @patch.object(context, 'unit_private_ip') @patch.object(context, 'neutron_plugin_attribute') def test_neutron_plumgrid_plugin_context(self, attr, ip, sec_groups): ip.return_value = '10.0.0.1' sec_groups.__get__ = MagicMock(return_value=True) attr.return_value = 'some.quantum.driver.class' neutron = context.NeutronContext() self.assertEquals({ 'config': 'some.quantum.driver.class', 'core_plugin': 'some.quantum.driver.class', 'neutron_plugin': 'plumgrid', 'neutron_security_groups': True, 'local_ip': '10.0.0.1'}, neutron.pg_ctxt()) @patch.object(context.NeutronContext, 'neutron_security_groups') @patch.object(context, 'unit_private_ip') @patch.object(context, 'neutron_plugin_attribute') def test_neutron_nuage_plugin_context(self, attr, ip, sec_groups): ip.return_value = '10.0.0.1' sec_groups.__get__ = MagicMock(return_value=True) attr.return_value = 'some.quantum.driver.class' neutron = context.NeutronContext() self.assertEquals({ 'config': 'some.quantum.driver.class', 'core_plugin': 'some.quantum.driver.class', 'neutron_plugin': 'vsp', 'neutron_security_groups': True, 'local_ip': '10.0.0.1'}, neutron.nuage_ctxt()) @patch.object(context.NeutronContext, 'neutron_security_groups') @patch.object(context, 'unit_private_ip') @patch.object(context, 'neutron_plugin_attribute') def test_neutron_midonet_plugin_context(self, attr, ip, sec_groups): ip.return_value = '10.0.0.1' sec_groups.__get__ = MagicMock(return_value=True) attr.return_value = 'some.quantum.driver.class' neutron = context.NeutronContext() self.assertEquals({ 'config': 'some.quantum.driver.class', 'core_plugin': 'some.quantum.driver.class', 'neutron_plugin': 'midonet', 'neutron_security_groups': True, 'local_ip': '10.0.0.1'}, neutron.midonet_ctxt()) @patch('charmhelpers.contrib.openstack.context.local_address') @patch.object(context.NeutronContext, 'network_manager') def test_neutron_neutron_ctxt(self, mock_network_manager, mock_local_address): vip = '88.11.22.33' priv_addr = '10.0.0.1' mock_local_address.return_value = priv_addr neutron = context.NeutronContext() config = {'vip': vip} self.config.side_effect = lambda key: config[key] mock_network_manager.__get__ = Mock(return_value='neutron') self.is_clustered.return_value = False self.assertEquals( {'network_manager': 'neutron', 'neutron_url': 'https://%s:9696' % (priv_addr)}, neutron.neutron_ctxt() ) self.is_clustered.return_value = True self.assertEquals( {'network_manager': 'neutron', 'neutron_url': 'https://%s:9696' % (vip)}, neutron.neutron_ctxt() ) @patch('charmhelpers.contrib.openstack.context.local_address') @patch.object(context.NeutronContext, 'network_manager') def test_neutron_neutron_ctxt_http(self, mock_network_manager, mock_local_address): vip = '88.11.22.33' priv_addr = '10.0.0.1' mock_local_address.return_value = priv_addr neutron = context.NeutronContext() config = {'vip': vip} self.config.side_effect = lambda key: config[key] self.https.return_value = False mock_network_manager.__get__ = Mock(return_value='neutron') self.is_clustered.return_value = False self.assertEquals( {'network_manager': 'neutron', 'neutron_url': 'http://%s:9696' % (priv_addr)}, neutron.neutron_ctxt() ) self.is_clustered.return_value = True self.assertEquals( {'network_manager': 'neutron', 'neutron_url': 'http://%s:9696' % (vip)}, neutron.neutron_ctxt() ) @patch.object(context.NeutronContext, 'neutron_ctxt') @patch.object(context.NeutronContext, 'ovs_ctxt') @patch.object(context.NeutronContext, 'plugin') @patch.object(context.NeutronContext, '_ensure_packages') @patch.object(context.NeutronContext, 'network_manager') def test_neutron_main_context_generation(self, mock_network_manager, mock_ensure_packages, mock_plugin, mock_ovs_ctxt, mock_neutron_ctxt): mock_neutron_ctxt.return_value = {'network_manager': 'neutron', 'neutron_url': 'https://foo:9696'} config = {'neutron-alchemy-flags': None} self.config.side_effect = lambda key: config[key] neutron = context.NeutronContext() mock_network_manager.__get__ = Mock(return_value='flatdhcpmanager') mock_plugin.__get__ = Mock() self.assertEquals({}, neutron()) self.assertTrue(mock_network_manager.__get__.called) self.assertFalse(mock_plugin.__get__.called) mock_network_manager.__get__.return_value = 'neutron' mock_plugin.__get__ = Mock(return_value=None) self.assertEquals({}, neutron()) self.assertTrue(mock_plugin.__get__.called) mock_ovs_ctxt.return_value = {'ovs': 'ovs_context'} mock_plugin.__get__.return_value = 'ovs' self.assertEquals( {'network_manager': 'neutron', 'ovs': 'ovs_context', 'neutron_url': 'https://foo:9696'}, neutron() ) @patch.object(context.NeutronContext, 'neutron_ctxt') @patch.object(context.NeutronContext, 'nvp_ctxt') @patch.object(context.NeutronContext, 'plugin') @patch.object(context.NeutronContext, '_ensure_packages') @patch.object(context.NeutronContext, 'network_manager') def test_neutron_main_context_gen_nvp_and_alchemy(self, mock_network_manager, mock_ensure_packages, mock_plugin, mock_nvp_ctxt, mock_neutron_ctxt): mock_neutron_ctxt.return_value = {'network_manager': 'neutron', 'neutron_url': 'https://foo:9696'} config = {'neutron-alchemy-flags': 'pool_size=20'} self.config.side_effect = lambda key: config[key] neutron = context.NeutronContext() mock_network_manager.__get__ = Mock(return_value='flatdhcpmanager') mock_plugin.__get__ = Mock() self.assertEquals({}, neutron()) self.assertTrue(mock_network_manager.__get__.called) self.assertFalse(mock_plugin.__get__.called) mock_network_manager.__get__.return_value = 'neutron' mock_plugin.__get__ = Mock(return_value=None) self.assertEquals({}, neutron()) self.assertTrue(mock_plugin.__get__.called) mock_nvp_ctxt.return_value = {'nvp': 'nvp_context'} mock_plugin.__get__.return_value = 'nvp' self.assertEquals( {'network_manager': 'neutron', 'nvp': 'nvp_context', 'neutron_alchemy_flags': {'pool_size': '20'}, 'neutron_url': 'https://foo:9696'}, neutron() ) @patch.object(context.NeutronContext, 'neutron_ctxt') @patch.object(context.NeutronContext, 'calico_ctxt') @patch.object(context.NeutronContext, 'plugin') @patch.object(context.NeutronContext, '_ensure_packages') @patch.object(context.NeutronContext, 'network_manager') def test_neutron_main_context_gen_calico(self, mock_network_manager, mock_ensure_packages, mock_plugin, mock_ovs_ctxt, mock_neutron_ctxt): mock_neutron_ctxt.return_value = {'network_manager': 'neutron', 'neutron_url': 'https://foo:9696'} config = {'neutron-alchemy-flags': None} self.config.side_effect = lambda key: config[key] neutron = context.NeutronContext() mock_network_manager.__get__ = Mock(return_value='flatdhcpmanager') mock_plugin.__get__ = Mock() self.assertEquals({}, neutron()) self.assertTrue(mock_network_manager.__get__.called) self.assertFalse(mock_plugin.__get__.called) mock_network_manager.__get__.return_value = 'neutron' mock_plugin.__get__ = Mock(return_value=None) self.assertEquals({}, neutron()) self.assertTrue(mock_plugin.__get__.called) mock_ovs_ctxt.return_value = {'Calico': 'calico_context'} mock_plugin.__get__.return_value = 'Calico' self.assertEquals( {'network_manager': 'neutron', 'Calico': 'calico_context', 'neutron_url': 'https://foo:9696'}, neutron() ) @patch('charmhelpers.contrib.openstack.utils.juju_log', lambda *args, **kwargs: None) @patch.object(context, 'config') def test_os_configflag_context(self, config): flags = context.OSConfigFlagContext() # single config.return_value = 'deadbeef=True' self.assertEquals({ 'user_config_flags': { 'deadbeef': 'True', } }, flags()) # multi config.return_value = 'floating_ip=True,use_virtio=False,max=5' self.assertEquals({ 'user_config_flags': { 'floating_ip': 'True', 'use_virtio': 'False', 'max': '5', } }, flags()) for empty in [None, '']: config.return_value = empty self.assertEquals({}, flags()) # multi with commas config.return_value = 'good_flag=woot,badflag,great_flag=w00t' self.assertEquals({ 'user_config_flags': { 'good_flag': 'woot,badflag', 'great_flag': 'w00t', } }, flags()) # missing key config.return_value = 'good_flag=woot=toow' self.assertRaises(context.OSContextError, flags) # bad value config.return_value = 'good_flag=woot==' self.assertRaises(context.OSContextError, flags) @patch.object(context, 'config') def test_os_configflag_context_custom(self, config): flags = context.OSConfigFlagContext( charm_flag='api-config-flags', template_flag='api_config_flags') # single config.return_value = 'deadbeef=True' self.assertEquals({ 'api_config_flags': { 'deadbeef': 'True', } }, flags()) def test_os_subordinate_config_context(self): relation = FakeRelation(relation_data=SUB_CONFIG_RELATION) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units nova_sub_ctxt = context.SubordinateConfigContext( service='nova', config_file='/etc/nova/nova.conf', interface='nova-subordinate', ) glance_sub_ctxt = context.SubordinateConfigContext( service='glance', config_file='/etc/glance/glance.conf', interface='glance-subordinate', ) cinder_sub_ctxt = context.SubordinateConfigContext( service='cinder', config_file='/etc/cinder/cinder.conf', interface='cinder-subordinate', ) foo_sub_ctxt = context.SubordinateConfigContext( service='foo', config_file='/etc/foo/foo.conf', interface='foo-subordinate', ) empty_sub_ctxt = context.SubordinateConfigContext( service='empty', config_file='/etc/foo/foo.conf', interface='empty-subordinate', ) self.assertEquals( nova_sub_ctxt(), {'sections': { 'DEFAULT': [ ['nova-key1', 'value1'], ['nova-key2', 'value2']] }} ) self.assertEquals( glance_sub_ctxt(), {'sections': { 'DEFAULT': [ ['glance-key1', 'value1'], ['glance-key2', 'value2']] }} ) self.assertEquals( cinder_sub_ctxt(), {'sections': { 'cinder-1-section': [ ['key1', 'value1']], 'cinder-2-section': [ ['key2', 'value2']] }, 'not-a-section': 1234} ) self.assertTrue( cinder_sub_ctxt.context_complete(cinder_sub_ctxt())) # subrodinate supplies nothing for given config glance_sub_ctxt.config_file = '/etc/glance/glance-api-paste.ini' self.assertEquals(glance_sub_ctxt(), {}) # subordinate supplies bad input self.assertEquals(foo_sub_ctxt(), {}) self.assertEquals(empty_sub_ctxt(), {}) self.assertFalse( empty_sub_ctxt.context_complete(empty_sub_ctxt())) def test_os_subordinate_config_context_multiple(self): relation = FakeRelation(relation_data=SUB_CONFIG_RELATION2) self.relation_get.side_effect = relation.get self.relation_ids.side_effect = relation.relation_ids self.related_units.side_effect = relation.relation_units nova_sub_ctxt = context.SubordinateConfigContext( service=['nova', 'nova-compute'], config_file='/etc/nova/nova.conf', interface=['nova-ceilometer', 'neutron-plugin'], ) self.assertEquals( nova_sub_ctxt(), {'sections': { 'DEFAULT': [ ['nova-key1', 'value1'], ['nova-key2', 'value2'], ['nova-key3', 'value3'], ['nova-key4', 'value4'], ['nova-key5', 'value5'], ['nova-key6', 'value6']] }} ) def test_syslog_context(self): self.config.side_effect = fake_config({'use-syslog': 'foo'}) syslog = context.SyslogContext() result = syslog() expected = { 'use_syslog': 'foo', } self.assertEquals(result, expected) def test_loglevel_context_set(self): self.config.side_effect = fake_config({ 'debug': True, 'verbose': True, }) syslog = context.LogLevelContext() result = syslog() expected = { 'debug': True, 'verbose': True, } self.assertEquals(result, expected) def test_loglevel_context_unset(self): self.config.side_effect = fake_config({ 'debug': None, 'verbose': None, }) syslog = context.LogLevelContext() result = syslog() expected = { 'debug': False, 'verbose': False, } self.assertEquals(result, expected) @patch.object(context, '_calculate_workers') def test_wsgi_worker_config_context(self, _calculate_workers): self.config.return_value = 2 # worker-multiplier=2 _calculate_workers.return_value = 8 service_name = 'service-name' script = '/usr/bin/script' ctxt = context.WSGIWorkerConfigContext(name=service_name, script=script) expect = { "service_name": service_name, "user": service_name, "group": service_name, "script": script, "admin_script": None, "public_script": None, "processes": 8, "admin_processes": 2, "public_processes": 6, "threads": 1, } self.assertEqual(expect, ctxt()) @patch.object(context, '_calculate_workers') def test_wsgi_worker_config_context_user_and_group(self, _calculate_workers): self.config.return_value = 1 _calculate_workers.return_value = 1 service_name = 'service-name' script = '/usr/bin/script' user = 'nova' group = 'nobody' ctxt = context.WSGIWorkerConfigContext(name=service_name, user=user, group=group, script=script) expect = { "service_name": service_name, "user": user, "group": group, "script": script, "admin_script": None, "public_script": None, "processes": 1, "admin_processes": 1, "public_processes": 1, "threads": 1, } self.assertEqual(expect, ctxt()) def test_zeromq_context_unrelated(self): self.is_relation_made.return_value = False self.assertEquals(context.ZeroMQContext()(), {}) def test_zeromq_context_related(self): self.is_relation_made.return_value = True self.relation_ids.return_value = ['zeromq-configuration:1'] self.related_units.return_value = ['openstack-zeromq/0'] self.relation_get.side_effect = ['nonce-data', 'hostname', 'redis'] self.assertEquals(context.ZeroMQContext()(), {'zmq_host': 'hostname', 'zmq_nonce': 'nonce-data', 'zmq_redis_address': 'redis'}) def test_notificationdriver_context_nomsg(self): relations = { 'zeromq-configuration': False, 'amqp': False, } rels = fake_is_relation_made(relations=relations) self.is_relation_made.side_effect = rels.rel_made self.assertEquals(context.NotificationDriverContext()(), {'notifications': 'False'}) def test_notificationdriver_context_zmq_nometer(self): relations = { 'zeromq-configuration': True, 'amqp': False, } rels = fake_is_relation_made(relations=relations) self.is_relation_made.side_effect = rels.rel_made self.assertEquals(context.NotificationDriverContext()(), {'notifications': 'False'}) def test_notificationdriver_context_zmq_meter(self): relations = { 'zeromq-configuration': True, 'amqp': False, } rels = fake_is_relation_made(relations=relations) self.is_relation_made.side_effect = rels.rel_made self.assertEquals(context.NotificationDriverContext()(), {'notifications': 'False'}) def test_notificationdriver_context_amq(self): relations = { 'zeromq-configuration': False, 'amqp': True, } rels = fake_is_relation_made(relations=relations) self.is_relation_made.side_effect = rels.rel_made self.assertEquals(context.NotificationDriverContext()(), {'notifications': 'True'}) @patch.object(context, 'psutil') def test_num_cpus_xenial(self, _psutil): _psutil.cpu_count.return_value = 4 self.assertEqual(context._num_cpus(), 4) @patch.object(context, 'psutil') def test_num_cpus_trusty(self, _psutil): _psutil.cpu_count.side_effect = AttributeError _psutil.NUM_CPUS = 4 self.assertEqual(context._num_cpus(), 4) @patch.object(context, '_num_cpus') def test_calculate_workers_float(self, _num_cpus): self.config.side_effect = fake_config({ 'worker-multiplier': 0.3 }) _num_cpus.return_value = 8 self.assertEqual(context._calculate_workers(), 2) @patch.object(context, '_num_cpus') def test_calculate_workers_float_negative(self, _num_cpus): self.config.side_effect = fake_config({ 'worker-multiplier': -4.0 }) _num_cpus.return_value = 8 self.assertEqual(context._calculate_workers(), 1) @patch.object(context, '_num_cpus') def test_calculate_workers_not_quite_0(self, _num_cpus): # Make sure that the multiplier evaluating to somewhere between # 0 and 1 in the floating point range still has at least one # worker. self.config.side_effect = fake_config({ 'worker-multiplier': 0.001 }) _num_cpus.return_value = 100 self.assertEqual(context._calculate_workers(), 1) @patch.object(context, '_num_cpus') def test_calculate_workers_0(self, _num_cpus): self.config.side_effect = fake_config({ 'worker-multiplier': 0 }) _num_cpus.return_value = 2 self.assertEqual(context._calculate_workers(), 1) @patch.object(context, '_num_cpus') def test_calculate_workers_noconfig(self, _num_cpus): self.config.return_value = None _num_cpus.return_value = 1 self.assertEqual(context._calculate_workers(), 2) @patch.object(context, '_num_cpus') def test_calculate_workers_noconfig_lotsa_cpus(self, _num_cpus): self.config.return_value = None _num_cpus.return_value = 32 self.assertEqual(context._calculate_workers(), 4) @patch.object(context, '_calculate_workers', return_value=256) def test_worker_context(self, calculate_workers): self.assertEqual(context.WorkerConfigContext()(), {'workers': 256}) def test_apache_get_addresses_no_network_config(self): self.config.side_effect = fake_config({ 'os-internal-network': None, 'os-admin-network': None, 'os-public-network': None }) self.resolve_address.return_value = '10.5.1.50' self.local_address.return_value = '10.5.1.50' apache = context.ApacheSSLContext() apache.external_ports = '8776' addresses = apache.get_network_addresses() expected = [('10.5.1.50', '10.5.1.50')] self.assertEqual(addresses, expected) self.get_address_in_network.assert_not_called() self.resolve_address.assert_has_calls([ call(context.INTERNAL), call(context.ADMIN), call(context.PUBLIC) ]) def test_apache_get_addresses_with_network_config(self): self.config.side_effect = fake_config({ 'os-internal-network': '10.5.1.0/24', 'os-admin-network': '10.5.2.0/24', 'os-public-network': '10.5.3.0/24', }) _base_addresses = ['10.5.1.100', '10.5.2.100', '10.5.3.100'] self.get_address_in_network.side_effect = _base_addresses self.resolve_address.side_effect = _base_addresses self.local_address.return_value = '10.5.1.50' apache = context.ApacheSSLContext() addresses = apache.get_network_addresses() expected = [('10.5.1.100', '10.5.1.100'), ('10.5.2.100', '10.5.2.100'), ('10.5.3.100', '10.5.3.100')] self.assertEqual(addresses, expected) calls = [call('10.5.1.0/24', '10.5.1.50'), call('10.5.2.0/24', '10.5.1.50'), call('10.5.3.0/24', '10.5.1.50')] self.get_address_in_network.assert_has_calls(calls) self.resolve_address.assert_has_calls([ call(context.INTERNAL), call(context.ADMIN), call(context.PUBLIC) ]) def test_apache_get_addresses_network_spaces(self): self.config.side_effect = fake_config({ 'os-internal-network': None, 'os-admin-network': None, 'os-public-network': None }) self.network_get_primary_address.side_effect = None self.network_get_primary_address.return_value = '10.5.2.50' self.resolve_address.return_value = '10.5.2.100' self.local_address.return_value = '10.5.1.50' apache = context.ApacheSSLContext() apache.external_ports = '8776' addresses = apache.get_network_addresses() expected = [('10.5.2.50', '10.5.2.100')] self.assertEqual(addresses, expected) self.get_address_in_network.assert_not_called() self.resolve_address.assert_has_calls([ call(context.INTERNAL), call(context.ADMIN), call(context.PUBLIC) ]) def test_config_flag_parsing_simple(self): # Standard key=value checks... flags = context.config_flags_parser('key1=value1, key2=value2') self.assertEqual(flags, {'key1': 'value1', 'key2': 'value2'}) # Check for multiple values to a single key flags = context.config_flags_parser('key1=value1, ' 'key2=value2,value3,value4') self.assertEqual(flags, {'key1': 'value1', 'key2': 'value2,value3,value4'}) # Check for yaml formatted key value pairings for more complex # assignment options. flags = context.config_flags_parser('key1: subkey1=value1,' 'subkey2=value2') self.assertEqual(flags, {'key1': 'subkey1=value1,subkey2=value2'}) # Check for good measure the ldap formats test_string = ('user_tree_dn: ou=ABC General,' 'ou=User Accounts,dc=example,dc=com') flags = context.config_flags_parser(test_string) self.assertEqual(flags, {'user_tree_dn': ('ou=ABC General,' 'ou=User Accounts,' 'dc=example,dc=com')}) def _fake_get_hwaddr(self, arg): return MACHINE_MACS[arg] def _fake_get_ipv4(self, arg, fatal=False): return MACHINE_NICS[arg] @patch('charmhelpers.contrib.openstack.context.config') def test_no_ext_port(self, mock_config): self.config.side_effect = config = fake_config({}) mock_config.side_effect = config self.assertEquals(context.ExternalPortContext()(), {}) @patch('charmhelpers.contrib.openstack.context.list_nics') @patch('charmhelpers.contrib.openstack.context.config') def test_ext_port_eth(self, mock_config, mock_list_nics): config = fake_config({'ext-port': 'eth1010'}) self.config.side_effect = config mock_config.side_effect = config mock_list_nics.return_value = ['eth1010'] self.assertEquals(context.ExternalPortContext()(), {'ext_port': 'eth1010'}) @patch('charmhelpers.contrib.openstack.context.list_nics') @patch('charmhelpers.contrib.openstack.context.config') def test_ext_port_eth_non_existent(self, mock_config, mock_list_nics): config = fake_config({'ext-port': 'eth1010'}) self.config.side_effect = config mock_config.side_effect = config mock_list_nics.return_value = [] self.assertEquals(context.ExternalPortContext()(), {}) @patch('charmhelpers.contrib.openstack.context.is_phy_iface', lambda arg: True) @patch('charmhelpers.contrib.openstack.context.get_nic_hwaddr') @patch('charmhelpers.contrib.openstack.context.list_nics') @patch('charmhelpers.contrib.openstack.context.get_ipv6_addr') @patch('charmhelpers.contrib.openstack.context.get_ipv4_addr') @patch('charmhelpers.contrib.openstack.context.config') def test_ext_port_mac(self, mock_config, mock_get_ipv4_addr, mock_get_ipv6_addr, mock_list_nics, mock_get_nic_hwaddr): config_macs = ABSENT_MACS + " " + MACHINE_MACS['eth2'] config = fake_config({'ext-port': config_macs}) self.config.side_effect = config mock_config.side_effect = config mock_get_ipv4_addr.side_effect = self._fake_get_ipv4 mock_get_ipv6_addr.return_value = [] mock_list_nics.return_value = MACHINE_MACS.keys() mock_get_nic_hwaddr.side_effect = self._fake_get_hwaddr self.assertEquals(context.ExternalPortContext()(), {'ext_port': 'eth2'}) config = fake_config({'ext-port': ABSENT_MACS}) self.config.side_effect = config mock_config.side_effect = config self.assertEquals(context.ExternalPortContext()(), {}) @patch('charmhelpers.contrib.openstack.context.is_phy_iface', lambda arg: True) @patch('charmhelpers.contrib.openstack.context.get_nic_hwaddr') @patch('charmhelpers.contrib.openstack.context.list_nics') @patch('charmhelpers.contrib.openstack.context.get_ipv6_addr') @patch('charmhelpers.contrib.openstack.context.get_ipv4_addr') @patch('charmhelpers.contrib.openstack.context.config') def test_ext_port_mac_one_used_nic(self, mock_config, mock_get_ipv4_addr, mock_get_ipv6_addr, mock_list_nics, mock_get_nic_hwaddr): self.relation_ids.return_value = ['neutron-plugin-api:1'] self.related_units.return_value = ['neutron-api/0'] self.relation_get.return_value = {'network-device-mtu': 1234, 'l2-population': 'False'} config_macs = "%s %s" % (MACHINE_MACS['eth1'], MACHINE_MACS['eth2']) mock_get_ipv4_addr.side_effect = self._fake_get_ipv4 mock_get_ipv6_addr.return_value = [] mock_list_nics.return_value = MACHINE_MACS.keys() mock_get_nic_hwaddr.side_effect = self._fake_get_hwaddr config = fake_config({'ext-port': config_macs}) self.config.side_effect = config mock_config.side_effect = config self.assertEquals(context.ExternalPortContext()(), {'ext_port': 'eth2', 'ext_port_mtu': 1234}) @patch('charmhelpers.contrib.openstack.context.NeutronPortContext.' 'resolve_ports') def test_data_port_eth(self, mock_resolve): self.config.side_effect = fake_config({'data-port': 'phybr1:eth1010 ' 'phybr1:eth1011'}) mock_resolve.side_effect = lambda ports: ['eth1010'] self.assertEquals(context.DataPortContext()(), {'eth1010': 'phybr1'}) @patch.object(context, 'get_nic_hwaddr') @patch.object(context.NeutronPortContext, 'resolve_ports') def test_data_port_mac(self, mock_resolve, mock_get_nic_hwaddr): extant_mac = 'cb:23:ae:72:f2:33' non_extant_mac = 'fa:16:3e:12:97:8e' self.config.side_effect = fake_config({'data-port': 'phybr1:%s phybr1:%s' % (non_extant_mac, extant_mac)}) def fake_resolve(ports): resolved = [] for port in ports: if port == extant_mac: resolved.append('eth1010') return resolved mock_get_nic_hwaddr.side_effect = lambda nic: extant_mac mock_resolve.side_effect = fake_resolve self.assertEquals(context.DataPortContext()(), {'eth1010': 'phybr1'}) @patch.object(context.NeutronAPIContext, '__call__', lambda *args: {'network_device_mtu': 5000}) @patch.object(context, 'get_nic_hwaddr', lambda inst, port: port) @patch.object(context.NeutronPortContext, 'resolve_ports', lambda inst, ports: ports) def test_phy_nic_mtu_context(self): self.config.side_effect = fake_config({'data-port': 'phybr1:eth0'}) ctxt = context.PhyNICMTUContext()() self.assertEqual(ctxt, {'devs': 'eth0', 'mtu': 5000}) @patch.object(context.glob, 'glob') @patch.object(context.NeutronAPIContext, '__call__', lambda *args: {'network_device_mtu': 5000}) @patch.object(context, 'get_nic_hwaddr', lambda inst, port: port) @patch.object(context.NeutronPortContext, 'resolve_ports', lambda inst, ports: ports) def test_phy_nic_mtu_context_vlan(self, mock_glob): self.config.side_effect = fake_config({'data-port': 'phybr1:eth0.100'}) mock_glob.return_value = ['/sys/class/net/eth0.100/lower_eth0'] ctxt = context.PhyNICMTUContext()() self.assertEqual(ctxt, {'devs': 'eth0\\neth0.100', 'mtu': 5000}) @patch.object(context.glob, 'glob') @patch.object(context.NeutronAPIContext, '__call__', lambda *args: {'network_device_mtu': 5000}) @patch.object(context, 'get_nic_hwaddr', lambda inst, port: port) @patch.object(context.NeutronPortContext, 'resolve_ports', lambda inst, ports: ports) def test_phy_nic_mtu_context_vlan_w_duplicate_raw(self, mock_glob): self.config.side_effect = fake_config({'data-port': 'phybr1:eth0.100 ' 'phybr1:eth0.200'}) def fake_glob(wcard): if 'eth0.100' in wcard: return ['/sys/class/net/eth0.100/lower_eth0'] elif 'eth0.200' in wcard: return ['/sys/class/net/eth0.200/lower_eth0'] raise Exception("Unexpeced key '%s'" % (wcard)) mock_glob.side_effect = fake_glob ctxt = context.PhyNICMTUContext()() self.assertEqual(ctxt, {'devs': 'eth0\\neth0.100\\neth0.200', 'mtu': 5000}) def test_neutronapicontext_defaults(self): self.relation_ids.return_value = [] expected_keys = [ 'l2_population', 'enable_dvr', 'enable_l3ha', 'overlay_network_type', 'network_device_mtu', 'enable_qos', 'enable_nsg_logging', 'global_physnet_mtu', 'physical_network_mtus' ] api_ctxt = context.NeutronAPIContext()() for key in expected_keys: self.assertTrue(key in api_ctxt) self.assertEquals(api_ctxt['polling_interval'], 2) self.assertEquals(api_ctxt['rpc_response_timeout'], 60) self.assertEquals(api_ctxt['report_interval'], 30) self.assertEquals(api_ctxt['enable_nsg_logging'], False) self.assertEquals(api_ctxt['global_physnet_mtu'], 1500) self.assertIsNone(api_ctxt['physical_network_mtus']) def setup_neutron_api_context_relation(self, cfg): self.relation_ids.return_value = ['neutron-plugin-api:1'] self.related_units.return_value = ['neutron-api/0'] # The l2-population key is used by the context as a way of checking if # the api service on the other end is sending data in a recent format. self.relation_get.return_value = cfg def test_neutronapicontext_extension_drivers_qos_on(self): self.setup_neutron_api_context_relation({ 'enable-qos': 'True', 'l2-population': 'True'}) api_ctxt = context.NeutronAPIContext()() self.assertTrue(api_ctxt['enable_qos']) self.assertEquals(api_ctxt['extension_drivers'], 'qos') def test_neutronapicontext_extension_drivers_qos_off(self): self.setup_neutron_api_context_relation({ 'enable-qos': 'False', 'l2-population': 'True'}) api_ctxt = context.NeutronAPIContext()() self.assertFalse(api_ctxt['enable_qos']) self.assertEquals(api_ctxt['extension_drivers'], '') def test_neutronapicontext_extension_drivers_qos_absent(self): self.setup_neutron_api_context_relation({ 'l2-population': 'True'}) api_ctxt = context.NeutronAPIContext()() self.assertFalse(api_ctxt['enable_qos']) self.assertEquals(api_ctxt['extension_drivers'], '') def test_neutronapicontext_extension_drivers_log_off(self): self.setup_neutron_api_context_relation({ 'enable-nsg-logging': 'False', 'l2-population': 'True'}) api_ctxt = context.NeutronAPIContext()() self.assertEquals(api_ctxt['extension_drivers'], '') def test_neutronapicontext_extension_drivers_log_on(self): self.setup_neutron_api_context_relation({ 'enable-nsg-logging': 'True', 'l2-population': 'True'}) api_ctxt = context.NeutronAPIContext()() self.assertEquals(api_ctxt['extension_drivers'], 'log') def test_neutronapicontext_extension_drivers_log_qos_on(self): self.setup_neutron_api_context_relation({ 'enable-qos': 'True', 'enable-nsg-logging': 'True', 'l2-population': 'True'}) api_ctxt = context.NeutronAPIContext()() self.assertEquals(api_ctxt['extension_drivers'], 'qos,log') def test_neutronapicontext_firewall_group_logging_on(self): self.setup_neutron_api_context_relation({ 'enable-nfg-logging': 'True', 'l2-population': 'True' }) api_ctxt = context.NeutronAPIContext()() self.assertEquals(api_ctxt['enable_nfg_logging'], True) def test_neutronapicontext_firewall_group_logging_off(self): self.setup_neutron_api_context_relation({ 'enable-nfg-logging': 'False', 'l2-population': 'True' }) api_ctxt = context.NeutronAPIContext()() self.assertEquals(api_ctxt['enable_nfg_logging'], False) def test_neutronapicontext_port_forwarding_on(self): self.setup_neutron_api_context_relation({ 'enable-port-forwarding': 'True', 'l2-population': 'True' }) api_ctxt = context.NeutronAPIContext()() self.assertEquals(api_ctxt['enable_port_forwarding'], True) def test_neutronapicontext_port_forwarding_off(self): self.setup_neutron_api_context_relation({ 'enable-port-forwarding': 'False', 'l2-population': 'True' }) api_ctxt = context.NeutronAPIContext()() self.assertEquals(api_ctxt['enable_port_forwarding'], False) def test_neutronapicontext_string_converted(self): self.setup_neutron_api_context_relation({ 'l2-population': 'True'}) api_ctxt = context.NeutronAPIContext()() self.assertEquals(api_ctxt['l2_population'], True) def test_neutronapicontext_none(self): self.relation_ids.return_value = ['neutron-plugin-api:1'] self.related_units.return_value = ['neutron-api/0'] self.relation_get.return_value = {'l2-population': 'True'} api_ctxt = context.NeutronAPIContext()() self.assertEquals(api_ctxt['network_device_mtu'], None) def test_network_service_ctxt_no_units(self): self.relation_ids.return_value = [] self.relation_ids.return_value = ['foo'] self.related_units.return_value = [] self.assertEquals(context.NetworkServiceContext()(), {}) @patch.object(context.OSContextGenerator, 'context_complete') def test_network_service_ctxt_no_data(self, mock_context_complete): rel = FakeRelation(QUANTUM_NETWORK_SERVICE_RELATION) self.relation_ids.side_effect = rel.relation_ids self.related_units.side_effect = rel.relation_units relation = FakeRelation(relation_data=QUANTUM_NETWORK_SERVICE_RELATION) self.relation_get.side_effect = relation.get mock_context_complete.return_value = False self.assertEquals(context.NetworkServiceContext()(), {}) def test_network_service_ctxt_data(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', 'api_version': '2.0', } rel = FakeRelation(QUANTUM_NETWORK_SERVICE_RELATION) self.relation_ids.side_effect = rel.relation_ids self.related_units.side_effect = rel.relation_units relation = FakeRelation(relation_data=QUANTUM_NETWORK_SERVICE_RELATION) self.relation_get.side_effect = relation.get self.assertEquals(context.NetworkServiceContext()(), data_result) def test_network_service_ctxt_data_api_version(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', 'api_version': '3', } rel = FakeRelation(QUANTUM_NETWORK_SERVICE_RELATION_VERSIONED) self.relation_ids.side_effect = rel.relation_ids self.related_units.side_effect = rel.relation_units relation = FakeRelation( relation_data=QUANTUM_NETWORK_SERVICE_RELATION_VERSIONED) self.relation_get.side_effect = relation.get self.assertEquals(context.NetworkServiceContext()(), data_result) def test_internal_endpoint_context(self): config = {'use-internal-endpoints': False} self.config.side_effect = fake_config(config) ctxt = context.InternalEndpointContext() self.assertFalse(ctxt()['use_internal_endpoints']) config = {'use-internal-endpoints': True} self.config.side_effect = fake_config(config) self.assertTrue(ctxt()['use_internal_endpoints']) @patch.object(context, 'os_release') def test_volume_api_context(self, mock_os_release): mock_os_release.return_value = 'ocata' config = {'use-internal-endpoints': False} self.config.side_effect = fake_config(config) ctxt = context.VolumeAPIContext('cinder-common') c = ctxt() self.assertEqual(c['volume_api_version'], '2') self.assertEqual(c['volume_catalog_info'], 'volumev2:cinderv2:publicURL') mock_os_release.return_value = 'pike' config['use-internal-endpoints'] = True self.config.side_effect = fake_config(config) ctxt = context.VolumeAPIContext('cinder-common') c = ctxt() self.assertEqual(c['volume_api_version'], '3') self.assertEqual(c['volume_catalog_info'], 'volumev3:cinderv3:internalURL') def test_volume_api_context_no_pkg(self): self.assertRaises(ValueError, context.VolumeAPIContext, "") self.assertRaises(ValueError, context.VolumeAPIContext, None) def test_apparmor_context_call_not_valid(self): ''' Tests for the apparmor context''' mock_aa_object = context.AppArmorContext() # Test with invalid config self.config.return_value = 'NOTVALID' self.assertEquals(mock_aa_object.__call__(), None) def test_apparmor_context_call_complain(self): ''' Tests for the apparmor context''' mock_aa_object = context.AppArmorContext() # Test complain mode self.config.return_value = 'complain' self.assertEquals(mock_aa_object.__call__(), {'aa_profile_mode': 'complain', 'ubuntu_release': '16.04'}) def test_apparmor_context_call_enforce(self): ''' Tests for the apparmor context''' mock_aa_object = context.AppArmorContext() # Test enforce mode self.config.return_value = 'enforce' self.assertEquals(mock_aa_object.__call__(), {'aa_profile_mode': 'enforce', 'ubuntu_release': '16.04'}) def test_apparmor_context_call_disable(self): ''' Tests for the apparmor context''' mock_aa_object = context.AppArmorContext() # Test complain mode self.config.return_value = 'disable' self.assertEquals(mock_aa_object.__call__(), {'aa_profile_mode': 'disable', 'ubuntu_release': '16.04'}) def test_apparmor_setup_complain(self): ''' Tests for the apparmor setup''' AA = context.AppArmorContext(profile_name='fake-aa-profile') AA.install_aa_utils = MagicMock() AA.manually_disable_aa_profile = MagicMock() # Test complain mode self.config.return_value = 'complain' AA.setup_aa_profile() AA.install_aa_utils.assert_called_with() self.check_call.assert_called_with(['aa-complain', 'fake-aa-profile']) self.assertFalse(AA.manually_disable_aa_profile.called) def test_apparmor_setup_enforce(self): ''' Tests for the apparmor setup''' AA = context.AppArmorContext(profile_name='fake-aa-profile') AA.install_aa_utils = MagicMock() AA.manually_disable_aa_profile = MagicMock() # Test enforce mode self.config.return_value = 'enforce' AA.setup_aa_profile() self.check_call.assert_called_with(['aa-enforce', 'fake-aa-profile']) self.assertFalse(AA.manually_disable_aa_profile.called) def test_apparmor_setup_disable(self): ''' Tests for the apparmor setup''' AA = context.AppArmorContext(profile_name='fake-aa-profile') AA.install_aa_utils = MagicMock() AA.manually_disable_aa_profile = MagicMock() # Test disable mode self.config.return_value = 'disable' AA.setup_aa_profile() self.check_call.assert_called_with(['aa-disable', 'fake-aa-profile']) self.assertFalse(AA.manually_disable_aa_profile.called) # Test failed to disable from subprocess import CalledProcessError self.check_call.side_effect = CalledProcessError(0, 0, 0) AA.setup_aa_profile() self.check_call.assert_called_with(['aa-disable', 'fake-aa-profile']) AA.manually_disable_aa_profile.assert_called_with() @patch.object(context, 'enable_memcache') @patch.object(context, 'is_ipv6_disabled') def test_memcache_context_ipv6(self, _is_ipv6_disabled, _enable_memcache): self.lsb_release.return_value = {'DISTRIB_CODENAME': 'xenial'} _enable_memcache.return_value = True _is_ipv6_disabled.return_value = False config = { 'openstack-origin': 'distro', } self.config.side_effect = fake_config(config) ctxt = context.MemcacheContext() self.assertTrue(ctxt()['use_memcache']) expect = { 'memcache_port': '11211', 'memcache_server': '::1', 'memcache_server_formatted': '[::1]', 'memcache_url': 'inet6:[::1]:11211', 'use_memcache': True} self.assertEqual(ctxt(), expect) self.lsb_release.return_value = {'DISTRIB_CODENAME': 'trusty'} expect['memcache_server'] = 'ip6-localhost' ctxt = context.MemcacheContext() self.assertEqual(ctxt(), expect) @patch.object(context, 'enable_memcache') @patch.object(context, 'is_ipv6_disabled') def test_memcache_context_ipv4(self, _is_ipv6_disabled, _enable_memcache): self.lsb_release.return_value = {'DISTRIB_CODENAME': 'xenial'} _enable_memcache.return_value = True _is_ipv6_disabled.return_value = True config = { 'openstack-origin': 'distro', } self.config.side_effect = fake_config(config) ctxt = context.MemcacheContext() self.assertTrue(ctxt()['use_memcache']) expect = { 'memcache_port': '11211', 'memcache_server': '127.0.0.1', 'memcache_server_formatted': '127.0.0.1', 'memcache_url': '127.0.0.1:11211', 'use_memcache': True} self.assertEqual(ctxt(), expect) self.lsb_release.return_value = {'DISTRIB_CODENAME': 'trusty'} expect['memcache_server'] = 'localhost' ctxt = context.MemcacheContext() self.assertEqual(ctxt(), expect) @patch.object(context, 'enable_memcache') def test_memcache_off_context(self, _enable_memcache): _enable_memcache.return_value = False config = {'openstack-origin': 'distro'} self.config.side_effect = fake_config(config) ctxt = context.MemcacheContext() self.assertFalse(ctxt()['use_memcache']) self.assertEqual(ctxt(), {'use_memcache': False}) @patch('charmhelpers.contrib.openstack.context.mkdir') def test_ensure_dir_ctx(self, mkdir): dirname = '/etc/keystone/policy.d' owner = 'someuser' group = 'somegroup' perms = 0o555 force = False ctxt = context.EnsureDirContext(dirname, owner=owner, group=group, perms=perms, force=force) ctxt() mkdir.assert_called_with(dirname, owner=owner, group=group, perms=perms, force=force) @patch.object(context, 'os_release') def test_VersionsContext(self, os_release): self.lsb_release.return_value = {'DISTRIB_CODENAME': 'xenial'} os_release.return_value = 'essex' self.assertEqual( context.VersionsContext()(), { 'openstack_release': 'essex', 'operating_system_release': 'xenial'}) os_release.assert_called_once_with('python-keystone') self.lsb_release.assert_called_once_with() def test_logrotate_context_unset(self): logrotate = context.LogrotateContext(location='nova', interval='weekly', count=4) ctxt = logrotate() expected_ctxt = { 'logrotate_logs_location': 'nova', 'logrotate_interval': 'weekly', 'logrotate_count': 'rotate 4', } self.assertEquals(ctxt, expected_ctxt) @patch.object(context, 'os_release') def test_vendordata_static(self, os_release): _vdata = '{"good": "json"}' os_release.return_value = 'rocky' self.config.side_effect = [_vdata, None] ctxt = context.NovaVendorMetadataContext('nova-common')() self.assertTrue(ctxt['vendor_data']) self.assertEqual('StaticJSON', ctxt['vendordata_providers']) self.assertNotIn('vendor_data_url', ctxt) @patch.object(context, 'os_release') def test_vendordata_dynamic(self, os_release): _vdata_url = 'http://example.org/vdata' os_release.return_value = 'rocky' self.config.side_effect = [None, _vdata_url] ctxt = context.NovaVendorMetadataContext('nova-common')() self.assertEqual(_vdata_url, ctxt['vendor_data_url']) self.assertEqual('DynamicJSON', ctxt['vendordata_providers']) self.assertFalse(ctxt['vendor_data']) @patch.object(context, 'os_release') def test_vendordata_static_and_dynamic(self, os_release): os_release.return_value = 'rocky' _vdata = '{"good": "json"}' _vdata_url = 'http://example.org/vdata' self.config.side_effect = [_vdata, _vdata_url] ctxt = context.NovaVendorMetadataContext('nova-common')() self.assertTrue(ctxt['vendor_data']) self.assertEqual(_vdata_url, ctxt['vendor_data_url']) self.assertEqual('StaticJSON,DynamicJSON', ctxt['vendordata_providers']) @patch.object(context, 'log') @patch.object(context, 'os_release') def test_vendordata_static_invalid_and_dynamic(self, os_release, log): os_release.return_value = 'rocky' _vdata = '{bad: json}' _vdata_url = 'http://example.org/vdata' self.config.side_effect = [_vdata, _vdata_url] ctxt = context.NovaVendorMetadataContext('nova-common')() self.assertFalse(ctxt['vendor_data']) self.assertEqual(_vdata_url, ctxt['vendor_data_url']) self.assertEqual('DynamicJSON', ctxt['vendordata_providers']) self.assertTrue(log.called) @patch('charmhelpers.contrib.openstack.context.log') @patch.object(context, 'os_release') def test_vendordata_static_and_dynamic_mitaka(self, os_release, log): os_release.return_value = 'mitaka' _vdata = '{"good": "json"}' _vdata_url = 'http://example.org/vdata' self.config.side_effect = [_vdata, _vdata_url] ctxt = context.NovaVendorMetadataContext('nova-common')() self.assertTrue(log.called) self.assertTrue(ctxt['vendor_data']) self.assertNotIn('vendor_data_url', ctxt) self.assertNotIn('vendordata_providers', ctxt) @patch.object(context, 'log') def test_vendordata_json_valid(self, log): _vdata = '{"good": "json"}' self.config.side_effect = [_vdata] ctxt = context.NovaVendorMetadataJSONContext('nova-common')() self.assertEqual({'vendor_data_json': _vdata}, ctxt) self.assertFalse(log.called) @patch.object(context, 'log') def test_vendordata_json_invalid(self, log): _vdata = '{bad: json}' self.config.side_effect = [_vdata] ctxt = context.NovaVendorMetadataJSONContext('nova-common')() self.assertEqual({'vendor_data_json': '{}'}, ctxt) self.assertTrue(log.called) @patch.object(context, 'log') def test_vendordata_json_empty(self, log): self.config.side_effect = [None] ctxt = context.NovaVendorMetadataJSONContext('nova-common')() self.assertEqual({'vendor_data_json': '{}'}, ctxt) self.assertFalse(log.called) @patch.object(context, 'socket') def test_host_info_context(self, _socket): _socket.getaddrinfo.return_value = [(None, None, None, 'myhost.mydomain', None)] _socket.gethostname.return_value = 'myhost' ctxt = context.HostInfoContext()() self.assertEqual({ 'host_fqdn': 'myhost.mydomain', 'host': 'myhost', 'use_fqdn_hint': False}, ctxt) ctxt = context.HostInfoContext(use_fqdn_hint_cb=lambda: True)() self.assertEqual({ 'host_fqdn': 'myhost.mydomain', 'host': 'myhost', 'use_fqdn_hint': True}, ctxt) # if getaddrinfo is unable to find the canonical name we should return # the shortname to match the behaviour of the original implementation. _socket.getaddrinfo.return_value = [(None, None, None, 'localhost', None)] ctxt = context.HostInfoContext()() self.assertEqual({ 'host_fqdn': 'myhost', 'host': 'myhost', 'use_fqdn_hint': False}, ctxt) if six.PY2: _socket.error = Exception _socket.getaddrinfo.side_effect = Exception else: _socket.getaddrinfo.side_effect = OSError _socket.gethostname.return_value = 'myhost' ctxt = context.HostInfoContext()() self.assertEqual({ 'host_fqdn': 'myhost', 'host': 'myhost', 'use_fqdn_hint': False}, ctxt) @patch.object(context, "DHCPAgentContext") def test_validate_ovs_use_veth(self, _context): # No existing dhcp_agent.ini and no config _context.get_existing_ovs_use_veth.return_value = None _context.parse_ovs_use_veth.return_value = None self.assertEqual((None, None), context.validate_ovs_use_veth()) # No existing dhcp_agent.ini and config set _context.get_existing_ovs_use_veth.return_value = None _context.parse_ovs_use_veth.return_value = True self.assertEqual((None, None), context.validate_ovs_use_veth()) # Existing dhcp_agent.ini and no config _context.get_existing_ovs_use_veth.return_value = True _context.parse_ovs_use_veth.return_value = None self.assertEqual((None, None), context.validate_ovs_use_veth()) # Check for agreement with existing dhcp_agent.ini _context.get_existing_ovs_use_veth.return_value = False _context.parse_ovs_use_veth.return_value = False self.assertEqual((None, None), context.validate_ovs_use_veth()) # Check for disagreement with existing dhcp_agent.ini _context.get_existing_ovs_use_veth.return_value = True _context.parse_ovs_use_veth.return_value = False self.assertEqual( ("blocked", "Mismatched existing and configured ovs-use-veth. See log."), context.validate_ovs_use_veth()) def test_dhcp_agent_context(self): # Defaults _config = { "debug": False, "dns-servers": None, "enable-isolated-metadata": None, "enable-metadata-network": None, "instance-mtu": None, "ovs-use-veth": None} _expect = { "append_ovs_config": False, "debug": False, "dns_servers": None, "enable_isolated_metadata": None, "enable_metadata_network": None, "instance_mtu": None, "ovs_use_veth": False} self.config.side_effect = fake_config(_config) _get_ovs_use_veth = MagicMock() _get_ovs_use_veth.return_value = False ctx_object = context.DHCPAgentContext() ctx_object.get_ovs_use_veth = _get_ovs_use_veth ctxt = ctx_object() self.assertEqual(_expect, ctxt) # Non-defaults _dns = "10.5.0.2" _mtu = 8950 _config = { "debug": True, "dns-servers": _dns, "enable-isolated-metadata": True, "enable-metadata-network": True, "instance-mtu": _mtu, "ovs-use-veth": True} _expect = { "append_ovs_config": False, "debug": True, "dns_servers": _dns, "enable_isolated_metadata": True, "enable_metadata_network": True, "instance_mtu": _mtu, "ovs_use_veth": True} self.config.side_effect = fake_config(_config) _get_ovs_use_veth.return_value = True ctxt = ctx_object() self.assertEqual(_expect, ctxt) def test_dhcp_agent_context_no_dns_domain(self): _config = {"dns-servers": '8.8.8.8'} self.config.side_effect = fake_config(_config) self.relation_ids.return_value = ['rid1'] self.related_units.return_value = ['nova-compute/0'] self.relation_get.return_value = 'nova' self.assertEqual( context.DHCPAgentContext()(), {'instance_mtu': None, 'dns_servers': '8.8.8.8', 'ovs_use_veth': False, "enable_isolated_metadata": None, "enable_metadata_network": None, "debug": None, "append_ovs_config": False} ) def test_dhcp_agent_context_dnsmasq_flags(self): _config = {'dnsmasq-flags': 'dhcp-userclass=set:ipxe,iPXE,' 'dhcp-match=set:ipxe,175,' 'server=1.2.3.4'} self.config.side_effect = fake_config(_config) self.assertEqual( context.DHCPAgentContext()(), { 'dnsmasq_flags': collections.OrderedDict( [('dhcp-userclass', 'set:ipxe,iPXE'), ('dhcp-match', 'set:ipxe,175'), ('server', '1.2.3.4')]), 'instance_mtu': None, 'dns_servers': None, 'ovs_use_veth': False, "enable_isolated_metadata": None, "enable_metadata_network": None, "debug": None, "append_ovs_config": False, } ) def test_get_ovs_use_veth(self): _get_existing_ovs_use_veth = MagicMock() _parse_ovs_use_veth = MagicMock() ctx_object = context.DHCPAgentContext() ctx_object.get_existing_ovs_use_veth = _get_existing_ovs_use_veth ctx_object.parse_ovs_use_veth = _parse_ovs_use_veth # Default _get_existing_ovs_use_veth.return_value = None _parse_ovs_use_veth.return_value = None self.assertEqual(False, ctx_object.get_ovs_use_veth()) # Existing dhcp_agent.ini and no config _get_existing_ovs_use_veth.return_value = True _parse_ovs_use_veth.return_value = None self.assertEqual(True, ctx_object.get_ovs_use_veth()) # No existing dhcp_agent.ini and config set _get_existing_ovs_use_veth.return_value = None _parse_ovs_use_veth.return_value = False self.assertEqual(False, ctx_object.get_ovs_use_veth()) # Both set matching _get_existing_ovs_use_veth.return_value = True _parse_ovs_use_veth.return_value = True self.assertEqual(True, ctx_object.get_ovs_use_veth()) # Both set mismatch: existing overrides _get_existing_ovs_use_veth.return_value = False _parse_ovs_use_veth.return_value = True self.assertEqual(False, ctx_object.get_ovs_use_veth()) # Both set mismatch: existing overrides _get_existing_ovs_use_veth.return_value = True _parse_ovs_use_veth.return_value = False self.assertEqual(True, ctx_object.get_ovs_use_veth()) @patch.object(context, 'config_ini') @patch.object(context.os.path, 'isfile') def test_get_existing_ovs_use_veth(self, _is_file, _config_ini): _config = {"ovs-use-veth": None} self.config.side_effect = fake_config(_config) ctx_object = context.DHCPAgentContext() # Default _is_file.return_value = False self.assertEqual(None, ctx_object.get_existing_ovs_use_veth()) # Existing _is_file.return_value = True _config_ini.return_value = {"DEFAULT": {"ovs_use_veth": True}} self.assertEqual(True, ctx_object.get_existing_ovs_use_veth()) # Existing config_ini returns string _is_file.return_value = True _config_ini.return_value = {"DEFAULT": {"ovs_use_veth": "False"}} self.assertEqual(False, ctx_object.get_existing_ovs_use_veth()) @patch.object(context, 'bool_from_string') def test_parse_ovs_use_veth(self, _bool_from_string): _config = {"ovs-use-veth": None} self.config.side_effect = fake_config(_config) ctx_object = context.DHCPAgentContext() # Unset self.assertEqual(None, ctx_object.parse_ovs_use_veth()) _bool_from_string.assert_not_called() # Consider empty string unset _config = {"ovs-use-veth": ""} self.config.side_effect = fake_config(_config) self.assertEqual(None, ctx_object.parse_ovs_use_veth()) _bool_from_string.assert_not_called() # Lower true _bool_from_string.return_value = True _config = {"ovs-use-veth": "true"} self.config.side_effect = fake_config(_config) self.assertEqual(True, ctx_object.parse_ovs_use_veth()) _bool_from_string.assert_called_with("true") # Lower false _bool_from_string.return_value = False _bool_from_string.reset_mock() _config = {"ovs-use-veth": "false"} self.config.side_effect = fake_config(_config) self.assertEqual(False, ctx_object.parse_ovs_use_veth()) _bool_from_string.assert_called_with("false") # Upper True _bool_from_string.return_value = True _bool_from_string.reset_mock() _config = {"ovs-use-veth": "True"} self.config.side_effect = fake_config(_config) self.assertEqual(True, ctx_object.parse_ovs_use_veth()) _bool_from_string.assert_called_with("True") # Invalid _bool_from_string.reset_mock() _config = {"ovs-use-veth": "Invalid"} self.config.side_effect = fake_config(_config) _bool_from_string.side_effect = ValueError with self.assertRaises(ValueError): ctx_object.parse_ovs_use_veth() _bool_from_string.assert_called_with("Invalid") class MockPCIDevice(object): """Simple wrapper to mock pci.PCINetDevice class""" def __init__(self, address): self.pci_address = address TEST_CPULIST_1 = "0-3" TEST_CPULIST_2 = "0-7,16-23" TEST_CPULIST_3 = "0,4,8,12,16,20,24" DPDK_DATA_PORTS = ( "br-phynet3:fe:16:41:df:23:fe " "br-phynet1:fe:16:41:df:23:fd " "br-phynet2:fe:f2:d0:45:dc:66" ) BOND_MAPPINGS = ( "bond0:fe:16:41:df:23:fe " "bond0:fe:16:41:df:23:fd " "bond1:fe:f2:d0:45:dc:66" ) PCI_DEVICE_MAP = { 'fe:16:41:df:23:fd': MockPCIDevice('0000:00:1c.0'), 'fe:16:41:df:23:fe': MockPCIDevice('0000:00:1d.0'), } class TestDPDKUtils(tests.utils.BaseTestCase): def test_resolve_pci_from_mapping_config(self): # FIXME: need to mock out the unit key value store self.patch_object(context, 'config') self.config.side_effect = lambda x: { 'data-port': DPDK_DATA_PORTS, 'dpdk-bond-mappings': BOND_MAPPINGS, }.get(x) _pci_devices = Mock() _pci_devices.get_device_from_mac.side_effect = PCI_DEVICE_MAP.get self.patch_object(context, 'pci') self.pci.PCINetDevices.return_value = _pci_devices self.assertDictEqual( context.resolve_pci_from_mapping_config('data-port'), { '0000:00:1c.0': context.EntityMac( 'br-phynet1', 'fe:16:41:df:23:fd'), '0000:00:1d.0': context.EntityMac( 'br-phynet3', 'fe:16:41:df:23:fe'), }) self.config.assert_called_once_with('data-port') self.config.reset_mock() self.assertDictEqual( context.resolve_pci_from_mapping_config('dpdk-bond-mappings'), { '0000:00:1c.0': context.EntityMac( 'bond0', 'fe:16:41:df:23:fd'), '0000:00:1d.0': context.EntityMac( 'bond0', 'fe:16:41:df:23:fe'), }) self.config.assert_called_once_with('dpdk-bond-mappings') DPDK_PATCH = [ 'resolve_pci_from_mapping_config', 'glob', ] NUMA_CORES_SINGLE = { '0': [0, 1, 2, 3] } NUMA_CORES_MULTI = { '0': [0, 1, 2, 3], '1': [4, 5, 6, 7] } LSCPU_ONE_SOCKET = b""" # The following is the parsable format, which can be fed to other # programs. Each different item in every column has an unique ID # starting from zero. # Socket 0 0 0 0 """ LSCPU_TWO_SOCKET = b""" # The following is the parsable format, which can be fed to other # programs. Each different item in every column has an unique ID # starting from zero. # Socket 0 1 0 1 0 1 0 1 0 1 0 1 0 1 """ class TestOVSDPDKDeviceContext(tests.utils.BaseTestCase): def setUp(self): super(TestOVSDPDKDeviceContext, self).setUp() self.patch_object(context, 'config') self.config.side_effect = lambda x: { 'enable-dpdk': True, } self.target = context.OVSDPDKDeviceContext() def patch_target(self, attr, return_value=None): mocked = mock.patch.object(self.target, attr) self._patches[attr] = mocked started = mocked.start() started.return_value = return_value self._patches_start[attr] = started setattr(self, attr, started) def test__parse_cpu_list(self): self.assertEqual(self.target._parse_cpu_list(TEST_CPULIST_1), [0, 1, 2, 3]) self.assertEqual(self.target._parse_cpu_list(TEST_CPULIST_2), [0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23]) self.assertEqual(self.target._parse_cpu_list(TEST_CPULIST_3), [0, 4, 8, 12, 16, 20, 24]) def test__numa_node_cores(self): self.patch_target('_parse_cpu_list') self._parse_cpu_list.return_value = [0, 1, 2, 3] self.patch_object(context, 'glob') self.glob.glob.return_value = [ '/sys/devices/system/node/node0' ] with patch_open() as (_, mock_file): mock_file.read.return_value = TEST_CPULIST_1 self.target._numa_node_cores() self.assertEqual(self.target._numa_node_cores(), {'0': [0, 1, 2, 3]}) self.glob.glob.assert_called_with('/sys/devices/system/node/node*') self._parse_cpu_list.assert_called_with(TEST_CPULIST_1) def test_device_whitelist(self): """Test device whitelist generation""" self.patch_object( context, 'resolve_pci_from_mapping_config', return_value=collections.OrderedDict( sorted({ '0000:00:1c.0': 'br-data', '0000:00:1d.0': 'br-data', }.items()))) self.assertEqual(self.target.device_whitelist(), '-w 0000:00:1c.0 -w 0000:00:1d.0') self.resolve_pci_from_mapping_config.assert_has_calls([ call('data-port'), call('dpdk-bond-mappings'), ]) def test_socket_memory(self): """Test socket memory configuration""" self.patch_object(context, 'check_output') self.patch_object(context, 'config') self.config.side_effect = lambda x: { 'dpdk-socket-memory': 1024, }.get(x) self.check_output.return_value = LSCPU_ONE_SOCKET self.assertEqual(self.target.socket_memory(), '1024') self.check_output.return_value = LSCPU_TWO_SOCKET self.assertEqual(self.target.socket_memory(), '1024,1024') self.config.side_effect = lambda x: { 'dpdk-socket-memory': 2048, }.get(x) self.assertEqual(self.target.socket_memory(), '2048,2048') def test_cpu_mask(self): """Test generation of hex CPU masks""" self.patch_target('_numa_node_cores') self._numa_node_cores.return_value = NUMA_CORES_SINGLE self.config.side_effect = lambda x: { 'dpdk-socket-cores': 1, }.get(x) self.assertEqual(self.target.cpu_mask(), '0x01') self._numa_node_cores.return_value = NUMA_CORES_MULTI self.assertEqual(self.target.cpu_mask(), '0x11') self.config.side_effect = lambda x: { 'dpdk-socket-cores': 2, }.get(x) self.assertEqual(self.target.cpu_mask(), '0x33') def test_cpu_masks(self): self.patch_target('_numa_node_cores') self._numa_node_cores.return_value = NUMA_CORES_MULTI self.config.side_effect = lambda x: { 'dpdk-socket-cores': 1, 'pmd-socket-cores': 2, }.get(x) self.assertEqual( self.target.cpu_masks(), {'dpdk_lcore_mask': '0x11', 'pmd_cpu_mask': '0x66'}) def test_context_no_devices(self): """Ensure that DPDK is disable when no devices detected""" self.patch_object(context, 'resolve_pci_from_mapping_config') self.resolve_pci_from_mapping_config.return_value = {} self.assertEqual(self.target(), {}) self.resolve_pci_from_mapping_config.assert_has_calls([ call('data-port'), call('dpdk-bond-mappings'), ]) def test_context_devices(self): """Ensure DPDK is enabled when devices are detected""" self.patch_target('_numa_node_cores') self.patch_target('devices') self.devices.return_value = collections.OrderedDict(sorted({ '0000:00:1c.0': 'br-data', '0000:00:1d.0': 'br-data', }.items())) self._numa_node_cores.return_value = NUMA_CORES_SINGLE self.patch_object(context, 'check_output') self.check_output.return_value = LSCPU_ONE_SOCKET self.config.side_effect = lambda x: { 'dpdk-socket-cores': 1, 'dpdk-socket-memory': 1024, 'enable-dpdk': True, }.get(x) self.assertEqual(self.target(), { 'cpu_mask': '0x01', 'device_whitelist': '-w 0000:00:1c.0 -w 0000:00:1d.0', 'dpdk_enabled': True, 'socket_memory': '1024' }) class TestDPDKDeviceContext(tests.utils.BaseTestCase): _dpdk_bridges = { '0000:00:1c.0': 'br-data', '0000:00:1d.0': 'br-physnet1', } _dpdk_bonds = { '0000:00:1c.1': 'dpdk-bond0', '0000:00:1d.1': 'dpdk-bond0', } def setUp(self): super(TestDPDKDeviceContext, self).setUp() self.target = context.DPDKDeviceContext() self.patch_object(context, 'resolve_pci_from_mapping_config') self.resolve_pci_from_mapping_config.side_effect = [ self._dpdk_bridges, self._dpdk_bonds, ] def test_context(self): self.patch_object(context, 'config') self.config.side_effect = lambda x: { 'dpdk-driver': 'uio_pci_generic', }.get(x) devices = copy.deepcopy(self._dpdk_bridges) devices.update(self._dpdk_bonds) self.assertEqual(self.target(), { 'devices': devices, 'driver': 'uio_pci_generic' }) self.config.assert_called_with('dpdk-driver') def test_context_none_driver(self): self.patch_object(context, 'config') self.config.return_value = None self.assertEqual(self.target(), {}) self.config.assert_called_with('dpdk-driver') class TestBridgePortInterfaceMap(tests.utils.BaseTestCase): def test__init__(self): self.maxDiff = None self.patch_object(context, 'config') # system with three interfaces (eth0, eth1 and eth2) where # eth0 and eth1 is part of linux bond bond0. # Bridge mapping br-ex:eth2, br-provider1:bond0 self.config.side_effect = lambda x: { 'data-port': ( 'br-ex:eth2 ' 'br-provider1:00:00:5e:00:00:41 ' 'br-provider1:00:00:5e:00:00:40'), 'dpdk-bond-mappings': '', }.get(x) self.patch_object(context, 'resolve_pci_from_mapping_config') self.resolve_pci_from_mapping_config.side_effect = [ { '0000:00:1c.0': context.EntityMac( 'br-ex', '00:00:5e:00:00:42'), }, {}, ] self.patch_object(context, 'list_nics') self.list_nics.return_value = ['bond0', 'eth0', 'eth1', 'eth2'] self.patch_object(context, 'is_phy_iface') self.is_phy_iface.side_effect = lambda x: True if not x.startswith( 'bond') else False self.patch_object(context, 'get_bond_master') self.get_bond_master.side_effect = lambda x: 'bond0' if x in ( 'eth0', 'eth1') else None self.patch_object(context, 'get_nic_hwaddr') self.get_nic_hwaddr.side_effect = lambda x: { 'bond0': '00:00:5e:00:00:24', 'eth0': '00:00:5e:00:00:40', 'eth1': '00:00:5e:00:00:41', 'eth2': '00:00:5e:00:00:42', }.get(x) bpi = context.BridgePortInterfaceMap() self.maxDiff = None expect = { 'br-provider1': { 'bond0': { 'bond0': { 'type': 'system', }, }, }, 'br-ex': { 'eth2': { 'eth2': { 'type': 'system', }, }, }, } self.assertDictEqual(bpi._map, expect) # do it again but this time use the linux bond name instead of mac # addresses. self.config.side_effect = lambda x: { 'data-port': ( 'br-ex:eth2 ' 'br-provider1:bond0'), 'dpdk-bond-mappings': '', }.get(x) bpi = context.BridgePortInterfaceMap() self.assertDictEqual(bpi._map, expect) # and if a user asks for a purely virtual interface let's not stop them expect = { 'br-provider1': { 'bond0.1234': { 'bond0.1234': { 'type': 'system', }, }, }, 'br-ex': { 'eth2': { 'eth2': { 'type': 'system', }, }, }, } self.config.side_effect = lambda x: { 'data-port': ( 'br-ex:eth2 ' 'br-provider1:bond0.1234'), 'dpdk-bond-mappings': '', }.get(x) bpi = context.BridgePortInterfaceMap() self.assertDictEqual(bpi._map, expect) # system with three interfaces (eth0, eth1 and eth2) where we should # enable DPDK and create OVS bond of eth0 and eth1. # Bridge mapping br-ex:eth2 br-provider1:dpdk-bond0 self.config.side_effect = lambda x: { 'enable-dpdk': True, 'data-port': ( 'br-ex:00:00:5e:00:00:42 ' 'br-provider1:dpdk-bond0'), 'dpdk-bond-mappings': ( 'dpdk-bond0:00:00:5e:00:00:40 ' 'dpdk-bond0:00:00:5e:00:00:41'), }.get(x) self.resolve_pci_from_mapping_config.side_effect = [ { '0000:00:1c.0': context.EntityMac( 'br-ex', '00:00:5e:00:00:42'), }, { '0000:00:1d.0': context.EntityMac( 'dpdk-bond0', '00:00:5e:00:00:40'), '0000:00:1e.0': context.EntityMac( 'dpdk-bond0', '00:00:5e:00:00:41'), }, ] # once devices are bound to DPDK they disappear from the system list # of interfaces self.list_nics.return_value = [] bpi = context.BridgePortInterfaceMap(global_mtu=1500) self.assertDictEqual(bpi._map, { 'br-provider1': { 'dpdk-bond0': { 'dpdk-600a59e': { 'pci-address': '0000:00:1d.0', 'type': 'dpdk', 'mtu-request': '1500', }, 'dpdk-5fc1d91': { 'pci-address': '0000:00:1e.0', 'type': 'dpdk', 'mtu-request': '1500', }, }, }, 'br-ex': { 'dpdk-6204d33': { 'dpdk-6204d33': { 'pci-address': '0000:00:1c.0', 'type': 'dpdk', 'mtu-request': '1500', }, }, }, }) def test_wrong_bridges_keys_pattern(self): self.patch_object(context, 'config') # check "" pattern self.config.side_effect = lambda x: { 'data-port': ( 'incorrect_pattern'), 'dpdk-bond-mappings': '', }.get(x) with self.assertRaises(ValueError): context.BridgePortInterfaceMap() # check ": " pattern self.config.side_effect = lambda x: { 'data-port': ( 'br-ex:eth2 ' 'br-provider1'), 'dpdk-bond-mappings': '', }.get(x) with self.assertRaises(ValueError): context.BridgePortInterfaceMap() def test_add_interface(self): self.patch_object(context, 'config') self.config.return_value = '' ctx = context.BridgePortInterfaceMap() ctx.add_interface("br1", "bond1", "port1", ctx.interface_type.dpdk, "00:00:00:00:00:01", 1500) ctx.add_interface("br1", "bond1", "port2", ctx.interface_type.dpdk, "00:00:00:00:00:02", 1500) ctx.add_interface("br1", "bond2", "port3", ctx.interface_type.dpdk, "00:00:00:00:00:03", 1500) ctx.add_interface("br1", "bond2", "port4", ctx.interface_type.dpdk, "00:00:00:00:00:04", 1500) expected = ( 'br1', { 'bond1': { 'port1': { 'type': 'dpdk', 'pci-address': '00:00:00:00:00:01', 'mtu-request': '1500', }, 'port2': { 'type': 'dpdk', 'pci-address': '00:00:00:00:00:02', 'mtu-request': '1500', }, }, 'bond2': { 'port3': { 'type': 'dpdk', 'pci-address': '00:00:00:00:00:03', 'mtu-request': '1500', }, 'port4': { 'type': 'dpdk', 'pci-address': '00:00:00:00:00:04', 'mtu-request': '1500', }, }, }, ) for br, bonds in ctx.items(): self.maxDiff = None self.assertEqual(br, expected[0]) self.assertDictEqual(bonds, expected[1]) class TestBondConfig(tests.utils.BaseTestCase): def test_get_bond_config(self): self.patch_object(context, 'config') self.config.side_effect = lambda x: { 'dpdk-bond-config': ':active-backup bond1:balance-slb:off', }.get(x) bonds_config = context.BondConfig() self.assertEqual(bonds_config.get_bond_config('bond0'), {'mode': 'active-backup', 'lacp': 'active', 'lacp-time': 'fast' }) self.assertEqual(bonds_config.get_bond_config('bond1'), {'mode': 'balance-slb', 'lacp': 'off', 'lacp-time': 'fast' }) class TestSRIOVContext(tests.utils.BaseTestCase): class ObjectView(object): def __init__(self, _dict): self.__dict__ = _dict def test___init__(self): self.patch_object(context.pci, 'PCINetDevices') pci_devices = self.ObjectView({ 'pci_devices': [ self.ObjectView({ 'pci_address': '0000:81:00.0', 'sriov': True, 'interface_name': 'eth0', 'sriov_totalvfs': 16, }), self.ObjectView({ 'pci_address': '0000:81:00.1', 'sriov': True, 'interface_name': 'eth1', 'sriov_totalvfs': 32, }), self.ObjectView({ 'pci_address': '0000:3:00.0', 'sriov': False, 'interface_name': 'eth2', }), ] }) self.PCINetDevices.return_value = pci_devices self.patch_object(context, 'config') # auto sets up numvfs = totalvfs self.config.return_value = { 'sriov-numvfs': 'auto', } self.assertDictEqual(context.SRIOVContext()(), { 'eth0': 16, 'eth1': 32, }) # when sriov-device-mappings is used only listed devices are set up self.config.return_value = { 'sriov-numvfs': 'auto', 'sriov-device-mappings': 'physnet1:eth0', } self.assertDictEqual(context.SRIOVContext()(), { 'eth0': 16, }) self.config.return_value = { 'sriov-numvfs': 'eth0:8', 'sriov-device-mappings': 'physnet1:eth0', } self.assertDictEqual(context.SRIOVContext()(), { 'eth0': 8, }) self.config.return_value = { 'sriov-numvfs': 'eth1:8', } self.assertDictEqual(context.SRIOVContext()(), { 'eth1': 8, }) # setting a numvfs value higher than a nic supports will revert to # the nics max value self.config.return_value = { 'sriov-numvfs': 'eth1:64', } self.assertDictEqual(context.SRIOVContext()(), { 'eth1': 32, }) # devices listed in sriov-numvfs have precedence over # sriov-device-mappings and the limiter still works when both are used self.config.return_value = { 'sriov-numvfs': 'eth1:64', 'sriov-device-mappings': 'physnet:eth0', } self.assertDictEqual(context.SRIOVContext()(), { 'eth1': 32, }) # alternate config keys have effect self.config.return_value = { 'my-own-sriov-numvfs': 'auto', 'my-own-sriov-device-mappings': 'physnet1:eth0', } self.assertDictEqual( context.SRIOVContext( numvfs_key='my-own-sriov-numvfs', device_mappings_key='my-own-sriov-device-mappings')(), { 'eth0': 16, }) # blanket configuration works and respects limits self.config.return_value = { 'sriov-numvfs': '24', } self.assertDictEqual(context.SRIOVContext()(), { 'eth0': 16, 'eth1': 24, }) def test___call__(self): self.patch_object(context.pci, 'PCINetDevices') pci_devices = self.ObjectView({'pci_devices': []}) self.PCINetDevices.return_value = pci_devices self.patch_object(context, 'config') self.config.return_value = {'sriov-numvfs': 'auto'} ctxt_obj = context.SRIOVContext() ctxt_obj._map = {} self.assertDictEqual(ctxt_obj(), {}) def test_get_map(self): self.patch_object(context.pci, 'PCINetDevices') pci_devices = self.ObjectView({ 'pci_devices': [ self.ObjectView({ 'pci_address': '0000:81:00.0', 'sriov': True, 'interface_name': 'eth0', 'sriov_totalvfs': 16, }), self.ObjectView({ 'pci_address': '0000:81:00.1', 'sriov': True, 'interface_name': 'eth1', 'sriov_totalvfs': 32, }), self.ObjectView({ 'pci_address': '0000:3:00.0', 'sriov': False, 'interface_name': 'eth2', }), ] }) self.PCINetDevices.return_value = pci_devices self.patch_object(context, 'config') self.config.return_value = { 'sriov-numvfs': 'auto', } self.assertDictEqual(context.SRIOVContext().get_map, { '0000:81:00.0': context.SRIOVContext.PCIDeviceNumVFs( mock.ANY, 16), '0000:81:00.1': context.SRIOVContext.PCIDeviceNumVFs( mock.ANY, 32), }) class TestCephBlueStoreContext(tests.utils.BaseTestCase): def setUp(self): super(TestCephBlueStoreContext, self,).setUp() self.expected_config_map = { 'bluestore-compression-algorithm': 'fake-bca', 'bluestore-compression-mode': 'fake-bcm', 'bluestore-compression-required-ratio': 'fake-bcrr', 'bluestore-compression-min-blob-size': 'fake-bcmibs', 'bluestore-compression-min-blob-size-hdd': 'fake-bcmibsh', 'bluestore-compression-min-blob-size-ssd': 'fake-bcmibss', 'bluestore-compression-max-blob-size': 'fake-bcmabs', 'bluestore-compression-max-blob-size-hdd': 'fake-bcmabsh', 'bluestore-compression-max-blob-size-ssd': 'fake-bcmabss', } self.expected_op = { key.replace('bluestore-', ''): value for key, value in self.expected_config_map.items() } self.patch_object(context, 'config') self.config.return_value = self.expected_config_map def test___call__(self): ctxt = context.CephBlueStoreCompressionContext() self.assertDictEqual(ctxt(), { key.replace('-', '_'): value for key, value in self.expected_config_map.items() }) def test_get_op(self): ctxt = context.CephBlueStoreCompressionContext() self.assertDictEqual(ctxt.get_op(), self.expected_op) def test_get_kwargs(self): ctxt = context.CephBlueStoreCompressionContext() for arg in ctxt.get_kwargs().keys(): self.assertNotIn('-', arg, "get_kwargs() returned '-' in the key") def test_validate(self): self.patch_object(context.ch_ceph, 'BasePool') pool = MagicMock() self.BasePool.return_value = pool ctxt = context.CephBlueStoreCompressionContext() ctxt.validate() # the order for the Dict argument is unpredictable, match on ANY and # do separate check against call_args_list with assertDictEqual. self.BasePool.assert_called_once_with('dummy-service', op=mock.ANY) expected_op = self.expected_op.copy() expected_op.update({'name': 'dummy-name'}) self.assertDictEqual( self.BasePool.call_args_list[0][1]['op'], expected_op) pool.validate.assert_called_once_with()