ironic/ironic/tests/unit/conductor/test_base_manager.py
Dmitry Tantsur 7ead2067fc Collect periodic tasks from all enabled hardware interfaces
Currently we only collect periodic tasks from interfaces used in enabled
classic drivers. Meaning, periodics are not collected from interfaces
that are only used in hardware types. This patch corrects it.

This patch does not enable collection of periodic tasks from hardware
types, since we did not collect them from classic drivers. I don't
remember the reason for that, and we may want to fix it later.

Change-Id: Ib1963f3f67a758a6b2405387bfe7b3e30cc31ed8
Story: #2001884
Task: #14357
2018-04-24 13:01:47 +02:00

546 lines
24 KiB
Python

# 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.
"""Test class for Ironic BaseConductorManager."""
import collections
import eventlet
import futurist
from futurist import periodics
import mock
from oslo_config import cfg
from oslo_db import exception as db_exception
from oslo_utils import uuidutils
from ironic.common import driver_factory
from ironic.common import exception
from ironic.conductor import base_manager
from ironic.conductor import manager
from ironic.conductor import notification_utils
from ironic.conductor import task_manager
from ironic.drivers import fake_hardware
from ironic.drivers import generic
from ironic import objects
from ironic.objects import fields
from ironic.tests import base as tests_base
from ironic.tests.unit.conductor import mgr_utils
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.objects import utils as obj_utils
CONF = cfg.CONF
@mgr_utils.mock_record_keepalive
class StartStopTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
def test_start_registers_conductor(self):
self.assertRaises(exception.ConductorNotFound,
objects.Conductor.get_by_hostname,
self.context, self.hostname)
self._start_service()
res = objects.Conductor.get_by_hostname(self.context, self.hostname)
self.assertEqual(self.hostname, res['hostname'])
def test_start_clears_conductor_locks(self):
node = obj_utils.create_test_node(self.context,
reservation=self.hostname)
node.save()
self._start_service()
node.refresh()
self.assertIsNone(node.reservation)
def test_stop_unregisters_conductor(self):
self._start_service()
res = objects.Conductor.get_by_hostname(self.context, self.hostname)
self.assertEqual(self.hostname, res['hostname'])
self.service.del_host()
self.assertRaises(exception.ConductorNotFound,
objects.Conductor.get_by_hostname,
self.context, self.hostname)
def test_stop_doesnt_unregister_conductor(self):
self._start_service()
res = objects.Conductor.get_by_hostname(self.context, self.hostname)
self.assertEqual(self.hostname, res['hostname'])
self.service.del_host(deregister=False)
res = objects.Conductor.get_by_hostname(self.context, self.hostname)
self.assertEqual(self.hostname, res['hostname'])
@mock.patch.object(manager.ConductorManager, 'init_host')
def test_stop_uninitialized_conductor(self, mock_init):
self._start_service()
self.service.del_host()
@mock.patch.object(driver_factory.DriverFactory, '__getitem__',
lambda *args: mock.MagicMock())
@mock.patch.object(driver_factory, 'StorageInterfaceFactory')
@mock.patch.object(driver_factory, 'NetworkInterfaceFactory')
def test_start_registers_driver_names(self, net_factory,
storage_factory):
init_names = ['fake1', 'fake2']
restart_names = ['fake3', 'fake4']
df = driver_factory.DriverFactory()
with mock.patch.object(df._extension_manager, 'names') as mock_names:
# verify driver names are registered
self.config(enabled_drivers=init_names)
mock_names.return_value = init_names
self._start_service()
res = objects.Conductor.get_by_hostname(self.context,
self.hostname)
self.assertEqual(init_names, res['drivers'])
self._stop_service()
# verify that restart registers new driver names
self.config(enabled_drivers=restart_names)
mock_names.return_value = restart_names
self._start_service()
res = objects.Conductor.get_by_hostname(self.context,
self.hostname)
self.assertEqual(restart_names, res['drivers'])
self.assertEqual(2, net_factory.call_count)
self.assertEqual(2, storage_factory.call_count)
@mock.patch.object(driver_factory, 'all_interfaces', autospec=True)
@mock.patch.object(driver_factory, 'hardware_types', autospec=True)
@mock.patch.object(driver_factory, 'drivers', autospec=True)
def test_start_registers_driver_specific_tasks(self, mock_drivers,
mock_hw_types, mock_ifaces):
class TestInterface(object):
@periodics.periodic(spacing=100500)
def iface(self):
pass
class TestInterface2(object):
@periodics.periodic(spacing=100500)
def iface(self):
pass
class Driver(object):
core_interfaces = []
standard_interfaces = ['iface']
all_interfaces = core_interfaces + standard_interfaces
iface = TestInterface()
@periodics.periodic(spacing=42)
def task(self, context):
pass
driver = Driver()
iface1 = TestInterface()
iface2 = TestInterface2()
expected = [iface1.iface, iface2.iface]
mock_drivers.return_value = {'fake1': driver}
mock_ifaces.return_value = {
'management': {'fake1': iface1},
'power': {'fake2': iface2}
}
self._start_service(start_periodic_tasks=True)
tasks = {c[0] for c in self.service._periodic_task_callables}
for item in expected:
self.assertTrue(periodics.is_periodic(item))
self.assertIn(item, tasks)
# no periodic tasks from the Driver object
self.assertTrue(periodics.is_periodic(driver.task))
self.assertNotIn(driver.task, tasks)
@mock.patch.object(driver_factory.DriverFactory, '__init__')
def test_start_fails_on_missing_driver(self, mock_df):
mock_df.side_effect = exception.DriverNotFound('test')
with mock.patch.object(self.dbapi, 'register_conductor') as mock_reg:
self.assertRaises(exception.DriverNotFound,
self.service.init_host)
self.assertTrue(mock_df.called)
self.assertFalse(mock_reg.called)
def test_start_fails_on_no_enabled_interfaces(self):
self.config(enabled_boot_interfaces=[])
self.assertRaisesRegex(exception.ConfigInvalid,
'options enabled_boot_interfaces',
self.service.init_host)
def test_starts_without_enabled_hardware_types(self):
self.config(enabled_hardware_types=[])
self.config(enabled_boot_interfaces=[])
self._start_service()
@mock.patch.object(base_manager, 'LOG')
@mock.patch.object(driver_factory, 'HardwareTypesFactory')
@mock.patch.object(driver_factory, 'DriverFactory')
def test_start_fails_on_no_driver_or_hw_types(self, df_mock, ht_mock,
log_mock):
driver_factory_mock = mock.MagicMock(names=[])
df_mock.return_value = driver_factory_mock
ht_mock.return_value = driver_factory_mock
self.assertRaises(exception.NoDriversLoaded,
self.service.init_host)
self.assertTrue(log_mock.error.called)
df_mock.assert_called_once_with()
ht_mock.assert_called_once_with()
@mock.patch.object(base_manager, 'LOG')
@mock.patch.object(base_manager.BaseConductorManager, 'del_host',
autospec=True)
@mock.patch.object(driver_factory, 'DriverFactory')
def test_starts_with_only_dynamic_drivers(self, df_mock, del_mock,
log_mock):
# don't load any classic drivers
driver_factory_mock = mock.MagicMock(names=[])
df_mock.return_value = driver_factory_mock
self.service.init_host()
self.assertFalse(log_mock.error.called)
df_mock.assert_called_with()
self.assertFalse(del_mock.called)
@mock.patch.object(base_manager, 'LOG')
@mock.patch.object(base_manager.BaseConductorManager, 'del_host',
autospec=True)
@mock.patch.object(driver_factory, 'HardwareTypesFactory')
def test_starts_with_only_classic_drivers(self, ht_mock, del_mock,
log_mock):
# don't load any dynamic drivers
driver_factory_mock = mock.MagicMock(names=[])
ht_mock.return_value = driver_factory_mock
self.service.init_host()
self.assertFalse(log_mock.error.called)
ht_mock.assert_called_with()
self.assertFalse(del_mock.called)
@mock.patch.object(base_manager, 'LOG')
@mock.patch.object(base_manager.BaseConductorManager,
'_register_and_validate_hardware_interfaces')
@mock.patch.object(base_manager.BaseConductorManager, 'del_host')
def test_start_fails_hw_type_register(self, del_mock, reg_mock, log_mock):
reg_mock.side_effect = exception.DriverNotFound('hw-type')
self.assertRaises(exception.DriverNotFound,
self.service.init_host)
self.assertTrue(log_mock.error.called)
del_mock.assert_called_once_with()
@mock.patch.object(base_manager, 'LOG')
@mock.patch.object(driver_factory, 'HardwareTypesFactory')
@mock.patch.object(driver_factory, 'DriverFactory')
def test_start_fails_on_name_conflict(self, df_mock, ht_mock, log_mock):
driver_factory_mock = mock.MagicMock(names=['dupe-driver'])
df_mock.return_value = driver_factory_mock
ht_mock.return_value = driver_factory_mock
self.assertRaises(exception.DriverNameConflict,
self.service.init_host)
self.assertTrue(log_mock.error.called)
df_mock.assert_called_once_with()
ht_mock.assert_called_once_with()
def test_prevent_double_start(self):
self._start_service()
self.assertRaisesRegex(RuntimeError, 'already running',
self.service.init_host)
@mock.patch.object(base_manager, 'LOG')
def test_warning_on_low_workers_pool(self, log_mock):
CONF.set_override('workers_pool_size', 3, 'conductor')
self._start_service()
self.assertTrue(log_mock.warning.called)
@mock.patch.object(eventlet.greenpool.GreenPool, 'waitall')
def test_del_host_waits_on_workerpool(self, wait_mock):
self._start_service()
self.service.del_host()
self.assertTrue(wait_mock.called)
def test_start_fails_on_missing_config_for_configdrive(self):
"""Check to fail conductor on missing config options"""
missing_parameters_error = ("Parameters missing to make a "
"connection with radosgw")
CONF.set_override('configdrive_use_object_store', True,
group='deploy')
CONF.set_override('object_store_endpoint_type', 'radosgw',
group='deploy')
params = {'auth_url': 'http://1.2.3.4',
'username': 'foo', 'password': 'foo_pass'}
CONF.register_opts((cfg.StrOpt(x) for x in params),
group='swift')
for key, value in params.items():
test_params = params.copy()
test_params[key] = None
for test_key, test_value in test_params.items():
CONF.set_override(key, test_value, group='swift')
with self.assertRaisesRegex(exception.ConfigInvalid,
missing_parameters_error):
self._start_service()
def test_conductor_shutdown_flag(self):
self._start_service()
self.assertFalse(self.service._shutdown)
self.service.del_host()
self.assertTrue(self.service._shutdown)
class CheckInterfacesTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
def test__check_enabled_interfaces_success(self):
base_manager._check_enabled_interfaces()
def test__check_enabled_interfaces_failure(self):
self.config(enabled_boot_interfaces=[])
self.assertRaisesRegex(exception.ConfigInvalid,
'options enabled_boot_interfaces',
base_manager._check_enabled_interfaces)
def test__check_enabled_interfaces_skip_if_no_hw_types(self):
self.config(enabled_hardware_types=[])
self.config(enabled_boot_interfaces=[])
base_manager._check_enabled_interfaces()
class KeepAliveTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
def test__conductor_service_record_keepalive(self):
self._start_service()
# avoid wasting time at the event.wait()
CONF.set_override('heartbeat_interval', 0, 'conductor')
with mock.patch.object(self.dbapi, 'touch_conductor') as mock_touch:
with mock.patch.object(self.service._keepalive_evt,
'is_set') as mock_is_set:
mock_is_set.side_effect = [False, True]
self.service._conductor_service_record_keepalive()
mock_touch.assert_called_once_with(self.hostname)
def test__conductor_service_record_keepalive_failed_db_conn(self):
self._start_service()
# avoid wasting time at the event.wait()
CONF.set_override('heartbeat_interval', 0, 'conductor')
with mock.patch.object(self.dbapi, 'touch_conductor') as mock_touch:
mock_touch.side_effect = [None, db_exception.DBConnectionError(),
None]
with mock.patch.object(self.service._keepalive_evt,
'is_set') as mock_is_set:
mock_is_set.side_effect = [False, False, False, True]
self.service._conductor_service_record_keepalive()
self.assertEqual(3, mock_touch.call_count)
def test__conductor_service_record_keepalive_failed_error(self):
self._start_service()
# avoid wasting time at the event.wait()
CONF.set_override('heartbeat_interval', 0, 'conductor')
with mock.patch.object(self.dbapi, 'touch_conductor') as mock_touch:
mock_touch.side_effect = [None, Exception(),
None]
with mock.patch.object(self.service._keepalive_evt,
'is_set') as mock_is_set:
mock_is_set.side_effect = [False, False, False, True]
self.service._conductor_service_record_keepalive()
self.assertEqual(3, mock_touch.call_count)
class ManagerSpawnWorkerTestCase(tests_base.TestCase):
def setUp(self):
super(ManagerSpawnWorkerTestCase, self).setUp()
self.service = manager.ConductorManager('hostname', 'test-topic')
self.executor = mock.Mock(spec=futurist.GreenThreadPoolExecutor)
self.service._executor = self.executor
def test__spawn_worker(self):
self.service._spawn_worker('fake', 1, 2, foo='bar', cat='meow')
self.executor.submit.assert_called_once_with(
'fake', 1, 2, foo='bar', cat='meow')
def test__spawn_worker_none_free(self):
self.executor.submit.side_effect = futurist.RejectedSubmission()
self.assertRaises(exception.NoFreeConductorWorker,
self.service._spawn_worker, 'fake')
@mock.patch.object(objects.Conductor, 'unregister_all_hardware_interfaces',
autospec=True)
@mock.patch.object(objects.Conductor, 'register_hardware_interfaces',
autospec=True)
@mock.patch.object(driver_factory, 'default_interface', autospec=True)
@mock.patch.object(driver_factory, 'enabled_supported_interfaces',
autospec=True)
@mgr_utils.mock_record_keepalive
class RegisterInterfacesTestCase(mgr_utils.ServiceSetUpMixin,
db_base.DbTestCase):
def setUp(self):
super(RegisterInterfacesTestCase, self).setUp()
self._start_service()
def test__register_and_validate_hardware_interfaces(self,
esi_mock,
default_mock,
reg_mock,
unreg_mock):
# these must be same order as esi_mock side effect
hardware_types = collections.OrderedDict((
('fake-hardware', fake_hardware.FakeHardware()),
('manual-management', generic.ManualManagementHardware),
))
esi_mock.side_effect = [
collections.OrderedDict((
('management', ['fake', 'noop']),
('deploy', ['agent', 'iscsi']),
)),
collections.OrderedDict((
('management', ['fake']),
('deploy', ['agent', 'fake']),
)),
]
default_mock.side_effect = ('fake', 'agent', 'fake', 'agent')
expected_calls = [
mock.call(mock.ANY, 'fake-hardware', 'management',
['fake', 'noop'], 'fake'),
mock.call(mock.ANY, 'fake-hardware', 'deploy', ['agent', 'iscsi'],
'agent'),
mock.call(mock.ANY, 'manual-management', 'management', ['fake'],
'fake'),
mock.call(mock.ANY, 'manual-management', 'deploy',
['agent', 'fake'], 'agent'),
]
self.service._register_and_validate_hardware_interfaces(hardware_types)
unreg_mock.assert_called_once_with(mock.ANY)
# we're iterating over dicts, don't worry about order
reg_mock.assert_has_calls(expected_calls)
def test__register_and_validate_no_valid_default(self,
esi_mock,
default_mock,
reg_mock,
unreg_mock):
# these must be same order as esi_mock side effect
hardware_types = collections.OrderedDict((
('fake-hardware', fake_hardware.FakeHardware()),
))
esi_mock.side_effect = [
collections.OrderedDict((
('management', ['fake', 'noop']),
('deploy', ['agent', 'iscsi']),
)),
]
default_mock.side_effect = exception.NoValidDefaultForInterface("boo")
self.assertRaises(
exception.NoValidDefaultForInterface,
self.service._register_and_validate_hardware_interfaces,
hardware_types)
default_mock.assert_called_once_with(
hardware_types['fake-hardware'],
mock.ANY, driver_name='fake-hardware')
unreg_mock.assert_called_once_with(mock.ANY)
self.assertFalse(reg_mock.called)
class StartConsolesTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
@mock.patch.object(notification_utils, 'emit_console_notification')
def test__start_consoles(self, mock_notify):
obj_utils.create_test_node(self.context,
driver='fake',
console_enabled=True)
obj_utils.create_test_node(
self.context,
uuid=uuidutils.generate_uuid(),
driver='fake',
console_enabled=True
)
obj_utils.create_test_node(
self.context,
uuid=uuidutils.generate_uuid(),
driver='fake'
)
self._start_service()
with mock.patch.object(self.driver.console,
'start_console') as mock_start_console:
self.service._start_consoles(self.context)
self.assertEqual(2, mock_start_console.call_count)
mock_notify.assert_has_calls(
[mock.call(mock.ANY, 'console_restore',
fields.NotificationStatus.START),
mock.call(mock.ANY, 'console_restore',
fields.NotificationStatus.END)])
@mock.patch.object(notification_utils, 'emit_console_notification')
def test__start_consoles_no_console_enabled(self, mock_notify):
obj_utils.create_test_node(self.context,
driver='fake',
console_enabled=False)
self._start_service()
with mock.patch.object(self.driver.console,
'start_console') as mock_start_console:
self.service._start_consoles(self.context)
self.assertFalse(mock_start_console.called)
self.assertFalse(mock_notify.called)
@mock.patch.object(notification_utils, 'emit_console_notification')
def test__start_consoles_failed(self, mock_notify):
test_node = obj_utils.create_test_node(self.context,
driver='fake',
console_enabled=True)
self._start_service()
with mock.patch.object(self.driver.console,
'start_console') as mock_start_console:
mock_start_console.side_effect = Exception()
self.service._start_consoles(self.context)
mock_start_console.assert_called_once_with(mock.ANY)
test_node.refresh()
self.assertFalse(test_node.console_enabled)
self.assertIsNotNone(test_node.last_error)
mock_notify.assert_has_calls(
[mock.call(mock.ANY, 'console_restore',
fields.NotificationStatus.START),
mock.call(mock.ANY, 'console_restore',
fields.NotificationStatus.ERROR)])
@mock.patch.object(notification_utils, 'emit_console_notification')
@mock.patch.object(base_manager, 'LOG')
def test__start_consoles_node_locked(self, log_mock, mock_notify):
test_node = obj_utils.create_test_node(self.context,
driver='fake',
console_enabled=True,
reservation='fake-host')
self._start_service()
with mock.patch.object(self.driver.console,
'start_console') as mock_start_console:
self.service._start_consoles(self.context)
self.assertFalse(mock_start_console.called)
test_node.refresh()
self.assertTrue(test_node.console_enabled)
self.assertIsNone(test_node.last_error)
self.assertTrue(log_mock.warning.called)
self.assertFalse(mock_notify.called)
@mock.patch.object(notification_utils, 'emit_console_notification')
@mock.patch.object(base_manager, 'LOG')
def test__start_consoles_node_not_found(self, log_mock, mock_notify):
test_node = obj_utils.create_test_node(self.context,
driver='fake',
console_enabled=True)
self._start_service()
with mock.patch.object(task_manager, 'acquire') as mock_acquire:
mock_acquire.side_effect = exception.NodeNotFound(node='not found')
with mock.patch.object(self.driver.console,
'start_console') as mock_start_console:
self.service._start_consoles(self.context)
self.assertFalse(mock_start_console.called)
test_node.refresh()
self.assertTrue(test_node.console_enabled)
self.assertIsNone(test_node.last_error)
self.assertTrue(log_mock.warning.called)
self.assertFalse(mock_notify.called)