# Copyright 2016 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import importlib from mock import ( ANY, call, patch, MagicMock ) from nova_compute_hooks import update_nrpe_config from test_utils import CharmTestCase with patch('charmhelpers.contrib.hardening.harden.harden') as mock_dec: mock_dec.side_effect = (lambda *dargs, **dkwargs: lambda f: lambda *args, **kwargs: f(*args, **kwargs)) with patch("nova_compute_utils.restart_map"): with patch("nova_compute_utils.register_configs"): import nova_compute_hooks as hooks importlib.reload(hooks) TO_PATCH = [ # charmhelpers.core.hookenv 'Hooks', 'config', 'local_unit', 'log', 'is_relation_made', 'relation_get', 'relation_ids', 'relation_set', 'service_name', 'related_units', 'remote_service_name', # charmhelpers.core.host 'apt_install', 'apt_purge', 'apt_update', 'filter_installed_packages', 'restart_on_change', 'service_restart', # charmhelpers.contrib.openstack.utils 'configure_installation_source', 'openstack_upgrade_available', # charmhelpers.contrib.network.ip 'get_relation_ip', # nova_compute_context 'nova_metadata_requirement', # nova_compute_utils # 'PACKAGES', 'create_libvirt_secret', 'restart_map', 'determine_packages', 'import_authorized_keys', 'import_keystone_ca_cert', 'initialize_ssh_keys', 'migration_enabled', 'do_openstack_upgrade', 'public_ssh_key', 'register_configs', 'disable_shell', 'enable_shell', 'update_nrpe_config', 'network_manager', 'libvirt_daemon', 'configure_local_ephemeral_storage', # misc_utils 'ensure_ceph_keyring', 'execd_preinstall', 'assert_libvirt_rbd_imagebackend_allowed', 'is_request_complete', 'send_request_if_needed', 'destroy_libvirt_network', # socket 'gethostname', 'create_sysctl', 'install_hugepages', 'uuid', # unitdata 'unitdata', # templating 'render', ] class NovaComputeRelationsTests(CharmTestCase): def setUp(self): super(NovaComputeRelationsTests, self).setUp(hooks, TO_PATCH) self.config.side_effect = self.test_config.get self.filter_installed_packages.side_effect = \ MagicMock(side_effect=lambda pkgs: pkgs) self.gethostname.return_value = 'testserver' self.get_relation_ip.return_value = '10.0.0.50' def test_install_hook(self): repo = 'cloud:precise-grizzly' self.test_config.set('openstack-origin', repo) self.determine_packages.return_value = ['foo', 'bar'] hooks.install() self.configure_installation_source.assert_called_with(repo) self.assertTrue(self.apt_update.called) self.apt_install.assert_called_with(['foo', 'bar'], fatal=True) self.assertTrue(self.execd_preinstall.called) @patch.object(hooks, 'ceph_changed') @patch.object(hooks, 'neutron_plugin_joined') def test_config_changed_with_upgrade(self, neutron_plugin_joined, ceph_changed): self.openstack_upgrade_available.return_value = True def rel_ids(x): return {'neutron-plugin': ['rid1'], 'ceph': ['ceph:0']}.get(x, []) self.relation_ids.side_effect = rel_ids self.related_units.return_value = ['ceph/0'] self.migration_enabled.return_value = False hooks.config_changed() self.assertTrue(self.do_openstack_upgrade.called) neutron_plugin_joined.assert_called_with('rid1', remote_restart=True) ceph_changed.assert_called_with(rid='ceph:0', unit='ceph/0') self.configure_local_ephemeral_storage.assert_called_once_with() def test_config_changed_with_openstack_upgrade_action(self): self.openstack_upgrade_available.return_value = True self.test_config.set('action-managed-upgrade', True) self.migration_enabled.return_value = False hooks.config_changed() self.assertFalse(self.do_openstack_upgrade.called) @patch.object(hooks, 'neutron_plugin_joined') @patch.object(hooks, 'compute_joined') def test_config_changed_with_migration(self, compute_joined, neutron_plugin_joined): self.migration_enabled.return_value = True self.test_config.set('migration-auth-type', 'ssh') self.relation_ids.return_value = [ 'cloud-compute:0', 'cloud-compute:1' ] hooks.config_changed() ex = [ call('cloud-compute:0'), call('cloud-compute:1'), ] self.assertEqual(ex, compute_joined.call_args_list) self.assertTrue(self.initialize_ssh_keys.called) @patch.object(hooks, 'neutron_plugin_joined') @patch.object(hooks, 'compute_joined') def test_config_changed_with_resize(self, compute_joined, neutron_plugin_joined): self.test_config.set('enable-resize', True) self.migration_enabled.return_value = False self.relation_ids.return_value = [ 'cloud-compute:0', 'cloud-compute:1' ] hooks.config_changed() ex = [ call('cloud-compute:0'), call('cloud-compute:1'), ] self.assertEqual(ex, compute_joined.call_args_list) self.initialize_ssh_keys.assert_called_with(user='nova') self.enable_shell.assert_called_with(user='nova') @patch.object(hooks, 'neutron_plugin_joined') @patch.object(hooks, 'compute_joined') def test_config_changed_without_resize(self, compute_joined, neutron_plugin_joined): self.test_config.set('enable-resize', False) self.migration_enabled.return_value = False self.relation_ids.return_value = [ 'cloud-compute:0', 'cloud-compute:1' ] hooks.config_changed() ex = [ call('cloud-compute:0'), call('cloud-compute:1'), ] self.assertEqual(ex, compute_joined.call_args_list) self.disable_shell.assert_called_with(user='nova') @patch.object(hooks, 'compute_joined') def test_config_changed_no_upgrade_no_migration(self, compute_joined): self.openstack_upgrade_available.return_value = False self.migration_enabled.return_value = False hooks.config_changed() self.assertFalse(self.do_openstack_upgrade.called) self.assertFalse(compute_joined.called) @patch.object(hooks, 'compute_joined') def test_config_changed_with_sysctl(self, compute_joined): self.migration_enabled.return_value = False self.test_config.set( 'sysctl', '{ kernel.max_pid : "1337", vm.swappiness : 10 }') hooks.config_changed() self.create_sysctl.assert_called_with( "{kernel.max_pid: '1337', vm.swappiness: 10}\n", '/etc/sysctl.d/50-nova-compute.conf') @patch.object(hooks, 'compute_joined') def test_config_changed_with_sysctl_swappy_default(self, compute_joined): self.test_config.set( 'sysctl', '{ kernel.max_pid : "1337" }') self.migration_enabled.return_value = False hooks.config_changed() self.create_sysctl.assert_called_with( "{kernel.max_pid: '1337', vm.swappiness: 1}\n", '/etc/sysctl.d/50-nova-compute.conf') @patch.object(hooks, 'compute_joined') def test_config_changed_no_nrpe(self, compute_joined): self.openstack_upgrade_available.return_value = False self.migration_enabled.return_value = False self.is_relation_made.return_value = False hooks.config_changed() self.assertFalse(self.update_nrpe_config.called) @patch.object(hooks, 'compute_joined') def test_config_changed_nrpe(self, compute_joined): self.openstack_upgrade_available.return_value = False self.migration_enabled.return_value = False self.is_relation_made.return_value = True hooks.config_changed() self.assertTrue(self.update_nrpe_config.called) @patch.object(hooks, 'compute_joined') def test_config_changed_invalid_migration(self, compute_joined): self.migration_enabled.return_value = True self.test_config.set('migration-auth-type', 'none') with self.assertRaises(Exception) as context: hooks.config_changed() self.assertEqual( context.exception.message, 'Invalid migration-auth-type') @patch('nova_compute_hooks.nrpe') @patch('nova_compute_hooks.services') @patch('charmhelpers.core.hookenv') def test_nrpe_services_no_qemu_kvm(self, hookenv, services, nrpe): ''' The qemu-kvm service is not monitored by NRPE, since it's one-shot. ''' services.return_value = ['libvirtd', 'qemu-kvm', 'libvirt-bin'] update_nrpe_config() nrpe.add_init_service_checks.assert_called_with( ANY, ['libvirtd', 'libvirt-bin'], ANY) def test_amqp_joined(self): hooks.amqp_joined() self.relation_set.assert_called_with( username='nova', vhost='openstack', relation_id=None) @patch.object(hooks, 'CONFIGS') def test_amqp_changed_missing_relation_data(self, configs): configs.complete_contexts = MagicMock() configs.complete_contexts.return_value = [] hooks.amqp_changed() self.log.assert_called_with( 'amqp relation incomplete. Peer not ready?' ) def _amqp_test(self, configs, neutron=False): configs.complete_contexts = MagicMock() configs.complete_contexts.return_value = ['amqp'] configs.write = MagicMock() hooks.amqp_changed() @patch.object(hooks, 'CONFIGS') def test_amqp_changed_with_data_no_neutron(self, configs): self._amqp_test(configs) self.assertEqual([call('/etc/nova/nova.conf')], configs.write.call_args_list) def test_db_joined(self): self.is_relation_made.return_value = False hooks.db_joined() self.relation_set.assert_called_with(relation_id=None, nova_database='nova', nova_username='nova', nova_hostname='10.0.0.50') self.get_relation_ip.assert_called_with('shared-db') @patch.object(hooks, 'CONFIGS') def test_db_changed_missing_relation_data(self, configs): configs.complete_contexts = MagicMock() configs.complete_contexts.return_value = [] hooks.db_changed() self.log.assert_called_with( 'shared-db relation incomplete. Peer not ready?' ) def _shared_db_test(self, configs): configs.complete_contexts = MagicMock() configs.complete_contexts.return_value = ['shared-db'] configs.write = MagicMock() hooks.db_changed() @patch.object(hooks, 'CONFIGS') def test_db_changed_with_data(self, configs): self._shared_db_test(configs) self.assertEqual([call('/etc/nova/nova.conf')], configs.write.call_args_list) @patch.object(hooks, 'CONFIGS') def test_image_service_missing_relation_data(self, configs): configs.complete_contexts = MagicMock() configs.complete_contexts.return_value = [] hooks.image_service_changed() self.log.assert_called_with( 'image-service relation incomplete. Peer not ready?' ) @patch.object(hooks, 'CONFIGS') def test_image_service_with_relation_data(self, configs): configs.complete_contexts = MagicMock() configs.write = MagicMock() configs.complete_contexts.return_value = ['image-service'] hooks.image_service_changed() configs.write.assert_called_with('/etc/nova/nova.conf') def test_compute_joined_no_migration_no_resize(self): self.migration_enabled.return_value = False hooks.compute_joined() self.assertFalse(self.relation_set.called) def test_compute_joined_with_ssh_migration(self): self.migration_enabled.return_value = True self.test_config.set('migration-auth-type', 'ssh') self.public_ssh_key.return_value = 'foo' hooks.compute_joined() self.relation_set.assert_called_with(**{ 'relation_id': None, 'ssh_public_key': 'foo', 'migration_auth_type': 'ssh', 'hostname': 'testserver', 'private-address': '10.0.0.50', }) hooks.compute_joined(rid='cloud-compute:2') self.relation_set.assert_called_with(**{ 'relation_id': 'cloud-compute:2', 'ssh_public_key': 'foo', 'migration_auth_type': 'ssh', 'hostname': 'testserver', 'private-address': '10.0.0.50', }) def test_compute_joined_with_resize(self): self.migration_enabled.return_value = False self.test_config.set('enable-resize', True) self.public_ssh_key.return_value = 'bar' hooks.compute_joined() self.relation_set.assert_called_with(**{ 'relation_id': None, 'nova_ssh_public_key': 'bar', 'hostname': 'testserver', 'private-address': '10.0.0.50', }) hooks.compute_joined(rid='cloud-compute:2') self.relation_set.assert_called_with(**{ 'relation_id': 'cloud-compute:2', 'nova_ssh_public_key': 'bar', 'hostname': 'testserver', 'private-address': '10.0.0.50', }) def test_compute_changed(self): hooks.compute_changed() self.assertTrue(self.import_keystone_ca_cert.called) self.import_authorized_keys.assert_has_calls([ call(), call(user='nova', prefix='nova'), ]) def test_compute_changed_nonstandard_authorized_keys_path(self): self.migration_enabled.return_value = False self.test_config.set('enable-resize', True) hooks.compute_changed() self.import_authorized_keys.assert_called_with( user='nova', prefix='nova', ) def test_ceph_joined(self): self.libvirt_daemon.return_value = 'libvirt-bin' hooks.ceph_joined() self.apt_install.assert_called_with(['ceph-common'], fatal=True) self.service_restart.assert_called_with('libvirt-bin') self.libvirt_daemon.assert_called() @patch.object(hooks, 'CONFIGS') def test_ceph_changed_missing_relation_data(self, configs): configs.complete_contexts = MagicMock() configs.complete_contexts.return_value = [] hooks.ceph_changed() self.log.assert_called_with( 'ceph relation incomplete. Peer not ready?' ) @patch.object(hooks, 'CONFIGS') def test_ceph_changed_no_keyring(self, configs): configs.complete_contexts = MagicMock() configs.complete_contexts.return_value = ['ceph'] self.ensure_ceph_keyring.return_value = False hooks.ceph_changed() self.log.assert_called_with( 'Could not create ceph keyring: peer not ready?' ) @patch.object(hooks, 'mark_broker_action_done') @patch.object(hooks, 'is_broker_action_done') @patch('nova_compute_context.service_name') @patch.object(hooks, 'CONFIGS') def test_ceph_changed_with_key_and_relation_data(self, configs, service_name, is_broker_action_done, mark_broker_action_done): self.test_config.set('libvirt-image-backend', 'rbd') self.is_request_complete.return_value = True self.assert_libvirt_rbd_imagebackend_allowed.return_value = True configs.complete_contexts = MagicMock() configs.complete_contexts.return_value = ['ceph'] configs.write = MagicMock() service_name.return_value = 'nova-compute' self.ensure_ceph_keyring.return_value = True is_broker_action_done.return_value = False hooks.ceph_changed() self.assertTrue(mark_broker_action_done.called) ex = [ call('/var/lib/charm/nova-compute/ceph.conf'), call('/etc/ceph/secret.xml'), call('/etc/nova/nova.conf'), ] self.assertEqual(ex, configs.write.call_args_list) self.service_restart.assert_called_with('nova-compute') is_broker_action_done.return_value = True mark_broker_action_done.reset_mock() hooks.ceph_changed() self.assertFalse(mark_broker_action_done.called) @patch('charmhelpers.contrib.storage.linux.ceph.CephBrokerRq' '.add_op_request_access_to_group') @patch('charmhelpers.contrib.storage.linux.ceph.CephBrokerRq' '.add_op_create_pool') def test_get_ceph_request(self, mock_create_pool, mock_request_access): self.assert_libvirt_rbd_imagebackend_allowed.return_value = True self.test_config.set('rbd-pool', 'nova') self.test_config.set('ceph-osd-replication-count', 3) self.test_config.set('ceph-pool-weight', 28) hooks.get_ceph_request() mock_create_pool.assert_not_called() mock_request_access.assert_not_called() @patch('charmhelpers.contrib.storage.linux.ceph.CephBrokerRq' '.add_op_request_access_to_group') @patch('charmhelpers.contrib.storage.linux.ceph.CephBrokerRq' '.add_op_create_pool') def test_get_ceph_request_rbd(self, mock_create_pool, mock_request_access): self.assert_libvirt_rbd_imagebackend_allowed.return_value = True self.test_config.set('rbd-pool', 'nova') self.test_config.set('ceph-osd-replication-count', 3) self.test_config.set('ceph-pool-weight', 28) self.test_config.set('libvirt-image-backend', 'rbd') hooks.get_ceph_request() mock_create_pool.assert_called_with(name='nova', replica_count=3, weight=28, group='vms') mock_request_access.assert_not_called() @patch('charmhelpers.contrib.storage.linux.ceph.CephBrokerRq' '.add_op_request_access_to_group') @patch('charmhelpers.contrib.storage.linux.ceph.CephBrokerRq' '.add_op_create_pool') def test_get_ceph_request_perms(self, mock_create_pool, mock_request_access): self.assert_libvirt_rbd_imagebackend_allowed.return_value = True self.test_config.set('rbd-pool', 'nova') self.test_config.set('ceph-osd-replication-count', 3) self.test_config.set('ceph-pool-weight', 28) self.test_config.set('libvirt-image-backend', 'rbd') self.test_config.set('restrict-ceph-pools', True) hooks.get_ceph_request() mock_create_pool.assert_called_with(name='nova', replica_count=3, weight=28, group='vms') mock_request_access.assert_has_calls([ call(name='volumes', permission='rwx'), call(name='images', permission='rwx'), call(name='vms', permission='rwx'), ]) @patch.object(hooks, 'service_restart_handler') @patch.object(hooks, 'CONFIGS') def test_neutron_plugin_changed(self, configs, service_restart_handler): self.nova_metadata_requirement.return_value = (True, 'sharedsecret') hooks.neutron_plugin_changed() self.assertTrue(self.apt_update.called) self.apt_install.assert_called_with(['nova-api-metadata'], fatal=True) configs.write.assert_called_with('/etc/nova/nova.conf') service_restart_handler.assert_called_with( default_service='nova-compute') @patch.object(hooks, 'service_restart_handler') @patch.object(hooks, 'CONFIGS') def test_neutron_plugin_changed_nometa(self, configs, service_restart_handler): self.nova_metadata_requirement.return_value = (False, None) hooks.neutron_plugin_changed() self.apt_purge.assert_called_with('nova-api-metadata', fatal=True) configs.write.assert_called_with('/etc/nova/nova.conf') service_restart_handler.assert_called_with( default_service='nova-compute') @patch.object(hooks, 'service_restart_handler') @patch.object(hooks, 'CONFIGS') def test_neutron_plugin_changed_meta(self, configs, service_restart_handler): self.nova_metadata_requirement.return_value = (True, None) hooks.neutron_plugin_changed() self.apt_install.assert_called_with(['nova-api-metadata'], fatal=True) configs.write.assert_called_with('/etc/nova/nova.conf') service_restart_handler.assert_called_with( default_service='nova-compute') @patch.object(hooks, 'get_hugepage_number') def test_neutron_plugin_joined_relid(self, get_hugepage_number): get_hugepage_number.return_value = None hooks.neutron_plugin_joined(relid='relid23') expect_rel_settings = { 'hugepage_number': None, 'default_availability_zone': 'nova', } self.relation_set.assert_called_with( relation_id='relid23', **expect_rel_settings ) @patch.object(hooks, 'get_hugepage_number') def test_neutron_plugin_joined_huge(self, get_hugepage_number): get_hugepage_number.return_value = 12 hooks.neutron_plugin_joined() expect_rel_settings = { 'hugepage_number': 12, 'default_availability_zone': 'nova', } self.relation_set.assert_called_with( relation_id=None, **expect_rel_settings ) @patch.object(hooks, 'get_hugepage_number') def test_neutron_plugin_joined_remote_restart(self, get_hugepage_number): get_hugepage_number.return_value = None self.uuid.uuid4.return_value = 'e030b959-7207' hooks.neutron_plugin_joined(remote_restart=True) expect_rel_settings = { 'hugepage_number': None, 'restart-trigger': 'e030b959-7207', 'default_availability_zone': 'nova', } self.relation_set.assert_called_with( relation_id=None, **expect_rel_settings ) @patch.object(hooks, 'is_unit_paused_set') def test_service_restart_handler(self, is_unit_paused_set): self.relation_get.return_value = None mock_kv = MagicMock() mock_kv.get.return_value = None self.unitdata.kv.return_value = mock_kv hooks.service_restart_handler(default_service='foorbar') self.relation_get.assert_called_with( attribute='restart-nonce', unit=None, rid=None ) is_unit_paused_set.assert_not_called() @patch.object(hooks, 'is_unit_paused_set') def test_service_restart_handler_with_service(self, is_unit_paused_set): self.relation_get.side_effect = ['nonce', 'foobar-service'] mock_kv = MagicMock() mock_kv.get.return_value = None self.unitdata.kv.return_value = mock_kv is_unit_paused_set.return_value = False hooks.service_restart_handler() self.relation_get.assert_has_calls([ call(attribute='restart-nonce', unit=None, rid=None), call(attribute='remote-service', unit=None, rid=None), ]) self.service_restart.assert_called_with('foobar-service') mock_kv.set.assert_called_with('restart-nonce', 'nonce') self.assertTrue(mock_kv.flush.called) @patch.object(hooks, 'is_unit_paused_set') def test_service_restart_handler_when_paused(self, is_unit_paused_set): self.relation_get.side_effect = ['nonce', 'foobar-service'] mock_kv = MagicMock() mock_kv.get.return_value = None self.unitdata.kv.return_value = mock_kv is_unit_paused_set.return_value = True hooks.service_restart_handler() self.relation_get.assert_has_calls([ call(attribute='restart-nonce', unit=None, rid=None), ]) self.service_restart.assert_not_called() mock_kv.set.assert_called_with('restart-nonce', 'nonce') self.assertTrue(mock_kv.flush.called) def test_ceph_access_incomplete(self): self.relation_get.return_value = None self.test_config.set('virt-type', 'kvm') hooks.ceph_access() self.relation_get.assert_has_calls([ call('key', None, None), call('secret-uuid', None, None), ]) self.render.assert_not_called() self.create_libvirt_secret.assert_not_called() def test_ceph_access_lxd(self): self.relation_get.side_effect = ['mykey', 'uuid2'] self.remote_service_name.return_value = 'cinder-ceph' self.test_config.set('virt-type', 'lxd') hooks.ceph_access() self.relation_get.assert_has_calls([ call('key', None, None), call('secret-uuid', None, None), ]) self.render.assert_not_called() self.create_libvirt_secret.assert_not_called() self.ensure_ceph_keyring.assert_called_with( service='cinder-ceph', user='nova', group='nova', key='mykey' ) def test_ceph_access_complete(self): self.relation_get.side_effect = ['mykey', 'uuid2'] self.remote_service_name.return_value = 'cinder-ceph' self.test_config.set('virt-type', 'kvm') hooks.ceph_access() self.relation_get.assert_has_calls([ call('key', None, None), call('secret-uuid', None, None), ]) self.render.assert_called_with( 'secret.xml', '/etc/ceph/secret-cinder-ceph.xml', context={'ceph_secret_uuid': 'uuid2', 'service_name': 'cinder-ceph'} ) self.create_libvirt_secret.assert_called_with( secret_file='/etc/ceph/secret-cinder-ceph.xml', secret_uuid='uuid2', key='mykey', ) self.ensure_ceph_keyring.assert_called_with( service='cinder-ceph', user='nova', group='nova', key='mykey' ) def test_secrets_storage_relation_joined(self): self.get_relation_ip.return_value = '10.23.1.2' self.gethostname.return_value = 'testhost' hooks.secrets_storage_joined() self.get_relation_ip.assert_called_with('secrets-storage') self.relation_set.assert_called_with( relation_id=None, secret_backend='charm-vaultlocker', isolated=True, access_address='10.23.1.2', hostname='testhost' ) self.gethostname.assert_called_once_with() def test_secrets_storage_relation_changed(self,): self.relation_get.return_value = None hooks.secrets_storage_changed() self.configure_local_ephemeral_storage.assert_called_once_with() def test_cloud_credentials_joined(self): self.local_unit.return_value = 'nova-compute-cell1/2' hooks.cloud_credentials_joined() self.relation_set.assert_called_with(username='nova_compute_cell1') @patch.object(hooks, 'CONFIGS') def test_cloud_credentials_changed(self, mock_CONFIGS): hooks.cloud_credentials_changed() mock_CONFIGS.write.assert_called_with('/etc/nova/nova.conf')