From 6e8cd3163f3bcefafe7a8ef5b82251994ab896b1 Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Fri, 17 Mar 2017 15:37:07 +0000 Subject: [PATCH] [ansible] driver refactoring make driver much closer to AgentDeploy by reusing HeartbeatMixin and implementing other required methods This effectively requires ironic version > 7.0 Change-Id: I9ba5186ecdae49a17785a109341050c828c849cc --- ironic_staging_drivers/ansible/deploy.py | 198 +++--- .../playbooks/roles/deploy/tasks/main.yaml | 3 + .../tests/unit/ansible/test_deploy.py | 571 ++++++++++-------- .../ansible-new-ironic-12e74f80cd72dbe3.yaml | 2 +- 4 files changed, 404 insertions(+), 370 deletions(-) diff --git a/ironic_staging_drivers/ansible/deploy.py b/ironic_staging_drivers/ansible/deploy.py index ac029c7..d5cdfd2 100644 --- a/ironic_staging_drivers/ansible/deploy.py +++ b/ironic_staging_drivers/ansible/deploy.py @@ -38,11 +38,11 @@ from ironic.common.i18n import _LW from ironic.common import images from ironic.common import states from ironic.common import utils -from ironic.conductor import rpcapi from ironic.conductor import task_manager from ironic.conductor import utils as manager_utils from ironic.conf import CONF from ironic.drivers import base +from ironic.drivers.modules import agent_base_vendor as agent_base from ironic.drivers.modules import deploy_utils @@ -182,34 +182,6 @@ def _get_node_ip(task): return _get_node_ip_dhcp(task) -# some good code from agent -def _reboot_and_finish_deploy(task): - wait = CONF.ansible.post_deploy_get_power_state_retry_interval * 1000 - attempts = CONF.ansible.post_deploy_get_power_state_retries + 1 - - @retrying.retry( - stop_max_attempt_number=attempts, - retry_on_result=lambda state: state != states.POWER_OFF, - wait_fixed=wait - ) - def _wait_until_powered_off(task): - return task.driver.power.get_power_state(task) - - try: - _wait_until_powered_off(task) - except Exception as e: - LOG.warning(_LW('Failed to soft power off node %(node_uuid)s ' - 'in at least %(timeout)d seconds. Error: %(error)s'), - {'node_uuid': task.node.uuid, - 'timeout': (wait * (attempts - 1)) / 1000, - 'error': e}) - manager_utils.node_power_action(task, states.POWER_OFF) - - task.driver.network.remove_provisioning_network(task) - task.driver.network.configure_tenant_networks(task) - manager_utils.node_power_action(task, states.POWER_ON) - - def _prepare_extra_vars(host_list, variables=None): nodes_var = [] for node_uuid, ip, user, extra in host_list: @@ -403,49 +375,15 @@ def _get_clean_steps(node, interface=None, override_priorities=None): return steps -# taken from agent driver -def _notify_conductor_resume_clean(task): - LOG.debug('Sending RPC to conductor to resume cleaning for node %s', - task.node.uuid) - uuid = task.node.uuid - rpc = rpcapi.ConductorAPI() - topic = rpc.get_topic_for(task.node) - # Need to release the lock to let the conductor take it - task.release_resources() - rpc.continue_node_clean(task.context, uuid, topic=topic) - - -def _deploy(task, node_address): - """Internal function for deployment to a node.""" - notags = ['wait'] if CONF.ansible.use_ramdisk_callback else [] - node = task.node - LOG.debug('IP of node %(node)s is %(ip)s', - {'node': node.uuid, 'ip': node_address}) - iwdi = node.driver_internal_info.get('is_whole_disk_image') - variables = _prepare_variables(task) - if iwdi: - notags.append('parted') - else: - variables.update(_parse_partitioning_info(task.node)) - playbook, user, key = _parse_ansible_driver_info(task.node) - node_list = [(node.uuid, node_address, user, node.extra)] - extra_vars = _prepare_extra_vars(node_list, variables=variables) - - LOG.debug('Starting deploy on node %s', node.uuid) - # any caller should manage exceptions raised from here - _run_playbook(playbook, extra_vars, key, notags=notags) - LOG.info(_LI('Ansible complete deploy on node %s'), node.uuid) - - LOG.debug('Rebooting node %s to instance', node.uuid) - manager_utils.node_set_boot_device(task, 'disk', persistent=True) - _reboot_and_finish_deploy(task) - - task.driver.boot.clean_up_ramdisk(task) - - -class AnsibleDeploy(base.DeployInterface): +class AnsibleDeploy(agent_base.HeartbeatMixin, base.DeployInterface): """Interface for deploy-related actions.""" + def __init__(self): + super(AnsibleDeploy, self).__init__() + # NOTE(pas-ha) overriding agent creation as we won't be + # communicating with it, only processing heartbeats + self._client = None + def get_properties(self): """Return the properties of the interface.""" return COMMON_PROPERTIES @@ -469,6 +407,26 @@ class AnsibleDeploy(base.DeployInterface): 'parameters were missing') % node.uuid deploy_utils.check_for_missing_params(params, error_msg) + def _ansible_deploy(self, task, node_address): + """Internal function for deployment to a node.""" + notags = ['wait'] if CONF.ansible.use_ramdisk_callback else [] + node = task.node + LOG.debug('IP of node %(node)s is %(ip)s', + {'node': node.uuid, 'ip': node_address}) + variables = _prepare_variables(task) + iwdi = node.driver_internal_info.get('is_whole_disk_image') + if iwdi: + notags.append('parted') + else: + variables.update(_parse_partitioning_info(task.node)) + playbook, user, key = _parse_ansible_driver_info(task.node) + node_list = [(node.uuid, node_address, user, node.extra)] + extra_vars = _prepare_extra_vars(node_list, variables=variables) + + LOG.debug('Starting deploy on node %s', node.uuid) + # any caller should manage exceptions raised from here + _run_playbook(playbook, extra_vars, key, notags=notags) + @task_manager.require_exclusive_lock def deploy(self, task): """Perform a deployment to a node.""" @@ -479,7 +437,7 @@ class AnsibleDeploy(base.DeployInterface): node = task.node ip_addr = _get_node_ip_dhcp(task) try: - _deploy(task, ip_addr) + self._ansible_deploy(task, ip_addr) except Exception as e: error = _('Deploy failed for node %(node)s: ' 'Error: %(exc)s') % {'node': node.uuid, @@ -488,7 +446,7 @@ class AnsibleDeploy(base.DeployInterface): deploy_utils.set_failed_state(task, error, collect_logs=False) else: - LOG.info(_LI('Deployment to node %s done'), node.uuid) + self.reboot_to_instance(task) return states.DEPLOYDONE @task_manager.require_exclusive_lock @@ -631,52 +589,62 @@ class AnsibleDeploy(base.DeployInterface): task.driver.boot.clean_up_ramdisk(task) task.driver.network.remove_cleaning_network(task) - def heartbeat(self, task, callback_url): - """Method for ansible ramdisk callback.""" + def continue_deploy(self, task): + # NOTE(pas-ha) the lock should be already upgraded in heartbeat, + # just setting its purpose for better logging + task.upgrade_lock(purpose='deploy') + task.process_event('resume') + # NOTE(pas-ha) this method is called from heartbeat processing only, + # so we are sure we need this particular method, not the general one + node_address = _get_node_ip_heartbeat(task) + self._ansible_deploy(task, node_address) + self.reboot_to_instance(task) + + def reboot_to_instance(self, task): node = task.node - address = urlparse.urlparse(callback_url).netloc.split(':')[0] + LOG.info(_LI('Ansible complete deploy on node %s'), node.uuid) - if node.maintenance: - # this shouldn't happen often, but skip the rest if it does. - LOG.debug('Heartbeat from node %(node)s in maintenance mode; ' - 'not taking any action.', {'node': node.uuid}) - elif node.provision_state == states.DEPLOYWAIT: - LOG.debug('Heartbeat from %(node)s.', {'node': node.uuid}) - task.upgrade_lock(purpose='deploy') - node = task.node - task.process_event('resume') + LOG.debug('Rebooting node %s to instance', node.uuid) + manager_utils.node_set_boot_device(task, 'disk', persistent=True) + self.reboot_and_finish_deploy(task) + task.driver.boot.clean_up_ramdisk(task) + + def reboot_and_finish_deploy(self, task): + wait = CONF.ansible.post_deploy_get_power_state_retry_interval * 1000 + attempts = CONF.ansible.post_deploy_get_power_state_retries + 1 + + @retrying.retry( + stop_max_attempt_number=attempts, + retry_on_result=lambda state: state != states.POWER_OFF, + wait_fixed=wait + ) + def _wait_until_powered_off(task): + return task.driver.power.get_power_state(task) + + node = task.node + try: try: - _deploy(task, address) + _wait_until_powered_off(task) except Exception as e: - error = _('Deploy failed for node %(node)s: ' - 'Error: %(exc)s') % {'node': node.uuid, - 'exc': six.text_type(e)} - LOG.exception(error) - deploy_utils.set_failed_state(task, error, collect_logs=False) + LOG.warning(_LW('Failed to soft power off node %(node_uuid)s ' + 'in at least %(timeout)d seconds. ' + 'Error: %(error)s'), + {'node_uuid': task.node.uuid, + 'timeout': (wait * (attempts - 1)) / 1000, + 'error': e}) + # NOTE(pas-ha) flush is a part of deploy playbook + # so if it finished successfully we can safely + # power off the node out-of-band + manager_utils.node_power_action(task, states.POWER_OFF) - else: - LOG.info(_LI('Deployment to node %s done'), node.uuid) - task.process_event('done') + task.driver.network.remove_provisioning_network(task) + task.driver.network.configure_tenant_networks(task) + manager_utils.node_power_action(task, states.POWER_ON) + except Exception as e: + msg = (_('Error rebooting node %(node)s after deploy. ' + 'Error: %(error)s') % + {'node': node.uuid, 'error': e}) + agent_base.log_and_raise_deployment_error(task, msg) - elif node.provision_state == states.CLEANWAIT: - LOG.debug('Node %s just booted to start cleaning.', - node.uuid) - task.upgrade_lock(purpose='clean') - node = task.node - driver_internal_info = node.driver_internal_info - driver_internal_info['agent_url'] = callback_url - node.driver_internal_info = driver_internal_info - node.save() - try: - _notify_conductor_resume_clean(task) - except Exception as e: - error = _('cleaning failed for node %(node)s: ' - 'Error: %(exc)s') % {'node': node.uuid, - 'exc': six.text_type(e)} - LOG.exception(error) - manager_utils.cleaning_error_handler(task, error) - - else: - LOG.warning(_LW('Call back from %(node)s in invalid provision ' - 'state %(state)s'), - {'node': node.uuid, 'state': node.provision_state}) + task.process_event('done') + LOG.info(_LI('Deployment to node %s done'), task.node.uuid) diff --git a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/main.yaml b/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/main.yaml index 68f11cc..e099b79 100644 --- a/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/main.yaml +++ b/ironic_staging_drivers/ansible/playbooks/roles/deploy/tasks/main.yaml @@ -15,3 +15,6 @@ - include: grub.yaml tags: - parted + +- name: flush + command: sync diff --git a/ironic_staging_drivers/tests/unit/ansible/test_deploy.py b/ironic_staging_drivers/tests/unit/ansible/test_deploy.py index 58334aa..fd84207 100644 --- a/ironic_staging_drivers/tests/unit/ansible/test_deploy.py +++ b/ironic_staging_drivers/tests/unit/ansible/test_deploy.py @@ -10,9 +10,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -import os - from ironic.common import dhcp_factory from ironic.common import exception from ironic.common import states @@ -27,13 +24,11 @@ from ironic.tests.unit.db import base as db_base from ironic.tests.unit.objects import utils as object_utils from ironic_lib import utils as irlib_utils import mock -from oslo_config import cfg +from oslo_concurrency import processutils import six from ironic_staging_drivers.ansible import deploy as ansible_deploy -CONF = cfg.CONF - INSTANCE_INFO = { 'image_source': 'fake-image', @@ -146,70 +141,76 @@ class TestAnsibleMethods(db_base.DbTestCase): ip_dhcp_mock.assert_called_once_with(task) self.assertEqual('127.0.0.1', res) - @mock.patch.object(utils, 'node_power_action', autospec=True) - @mock.patch.object(fake.FakePower, 'get_power_state', - return_value=states.POWER_OFF) - def test__reboot_and_finish_deploy(self, get_pow_state_mock, - power_action_mock): - self.config(group='ansible', - post_deploy_get_power_state_retry_interval=0) + @mock.patch.object(com_utils, 'execute', return_value=('out', 'err'), + autospec=True) + def test__run_playbook(self, execute_mock): + self.config(group='ansible', playbooks_path='/path/to/playbooks') + self.config(group='ansible', config_file_path='/path/to/config') + self.config(group='ansible', verbosity=3) + self.config(group='ansible', ansible_extra_args='--timeout=100') + extra_vars = {'foo': 'bar'} - with task_manager.acquire(self.context, self.node.uuid) as task: - ansible_deploy._reboot_and_finish_deploy(task) - get_pow_state_mock.assert_called_once_with(task) - power_action_mock.assert_called_once_with(task, states.POWER_ON) + ansible_deploy._run_playbook('deploy', extra_vars, '/path/to/key', + tags=['spam'], notags=['ham']) - @mock.patch.object(utils, 'node_power_action', autospec=True) - @mock.patch.object(fake.FakePower, 'get_power_state', - return_value=states.POWER_ON) - def test__reboot_and_finish_deploy_retry(self, get_pow_state_mock, - power_action_mock): - self.config(group='ansible', - post_deploy_get_power_state_retry_interval=0) - - with task_manager.acquire(self.context, self.node.uuid) as task: - ansible_deploy._reboot_and_finish_deploy(task) - get_pow_state_mock.assert_called_with(task) - self.assertEqual( - CONF.ansible.post_deploy_get_power_state_retries + 1, - len(get_pow_state_mock.mock_calls)) - expected_power_calls = [((task, states.POWER_OFF),), - ((task, states.POWER_ON),)] - self.assertEqual(expected_power_calls, - power_action_mock.call_args_list) + execute_mock.assert_called_once_with( + 'env', 'ANSIBLE_CONFIG=/path/to/config', + 'ansible-playbook', '/path/to/playbooks/deploy', '-i', + ansible_deploy.INVENTORY_FILE, '-e', '{"foo": "bar"}', + '--tags=spam', '--skip-tags=ham', + '--private-key=/path/to/key', '-vvv', '--timeout=100') @mock.patch.object(com_utils, 'execute', return_value=('out', 'err'), autospec=True) - @mock.patch.object(os.path, 'join', return_value='/path/to/playbook', - autospec=True) - def test__run_playbook(self, path_join_mock, execute_mock): - extra_vars = {"ironic_nodes": [{"name": self.node["uuid"], - "ip": "127.0.0.1", "user": "test"}]} + def test__run_playbook_default_verbosity_nodebug(self, execute_mock): + self.config(group='ansible', playbooks_path='/path/to/playbooks') + self.config(group='ansible', config_file_path='/path/to/config') + self.config(debug=False) + extra_vars = {'foo': 'bar'} ansible_deploy._run_playbook('deploy', extra_vars, '/path/to/key') execute_mock.assert_called_once_with( - 'env', 'ANSIBLE_CONFIG=%s' % CONF.ansible.config_file_path, - 'ansible-playbook', '/path/to/playbook', '-i', - ansible_deploy.INVENTORY_FILE, '-e', json.dumps(extra_vars), - '--private-key=/path/to/key', '-vvvv') + 'env', 'ANSIBLE_CONFIG=/path/to/config', + 'ansible-playbook', '/path/to/playbooks/deploy', '-i', + ansible_deploy.INVENTORY_FILE, '-e', '{"foo": "bar"}', + '--private-key=/path/to/key') @mock.patch.object(com_utils, 'execute', return_value=('out', 'err'), autospec=True) - @mock.patch.object(os.path, 'join', return_value='/path/to/playbook', - autospec=True) - def test__run_playbook_tags(self, path_join_mock, execute_mock): - extra_vars = {"ironic_nodes": [{"name": self.node["uuid"], - "ip": "127.0.0.1", "user": "test"}]} + def test__run_playbook_default_verbosity_debug(self, execute_mock): + self.config(group='ansible', playbooks_path='/path/to/playbooks') + self.config(group='ansible', config_file_path='/path/to/config') + self.config(debug=True) + extra_vars = {'foo': 'bar'} - ansible_deploy._run_playbook('deploy', extra_vars, '/path/to/key', - tags=['wait']) + ansible_deploy._run_playbook('deploy', extra_vars, '/path/to/key') execute_mock.assert_called_once_with( - 'env', 'ANSIBLE_CONFIG=%s' % CONF.ansible.config_file_path, - 'ansible-playbook', '/path/to/playbook', '-i', - ansible_deploy.INVENTORY_FILE, '-e', json.dumps(extra_vars), - '--tags=wait', '--private-key=/path/to/key', '-vvvv') + 'env', 'ANSIBLE_CONFIG=/path/to/config', + 'ansible-playbook', '/path/to/playbooks/deploy', '-i', + ansible_deploy.INVENTORY_FILE, '-e', '{"foo": "bar"}', + '--private-key=/path/to/key', '-vvvv') + + @mock.patch.object(com_utils, 'execute', + side_effect=processutils.ProcessExecutionError( + description='VIKINGS!'), + autospec=True) + def test__run_playbook_fail(self, execute_mock): + self.config(group='ansible', playbooks_path='/path/to/playbooks') + self.config(group='ansible', config_file_path='/path/to/config') + self.config(debug=False) + extra_vars = {'foo': 'bar'} + + exc = self.assertRaises(exception.InstanceDeployFailure, + ansible_deploy._run_playbook, + 'deploy', extra_vars, '/path/to/key') + self.assertIn('VIKINGS!', six.text_type(exc)) + execute_mock.assert_called_once_with( + 'env', 'ANSIBLE_CONFIG=/path/to/config', + 'ansible-playbook', '/path/to/playbooks/deploy', '-i', + ansible_deploy.INVENTORY_FILE, '-e', '{"foo": "bar"}', + '--private-key=/path/to/key') def test__parse_partitioning_info(self): expected_info = { @@ -258,113 +259,102 @@ class TestAnsibleMethods(db_base.DbTestCase): 'ephemeral_format': 'ext4', 'preserve_ephemeral': 'yes' } - i_info = ansible_deploy._parse_partitioning_info(self.node) self.assertEqual(expected_info, i_info) - @mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk') - @mock.patch.object(ansible_deploy, '_reboot_and_finish_deploy', - autospec=True) - @mock.patch.object(utils, 'node_set_boot_device', autospec=True) - @mock.patch.object(ansible_deploy, '_run_playbook', autospec=True) - @mock.patch.object(ansible_deploy, '_prepare_extra_vars', autospec=True) - @mock.patch.object(ansible_deploy, '_parse_ansible_driver_info', - return_value=('test_pl', 'test_u', 'test_k'), - autospec=True) - @mock.patch.object(ansible_deploy, '_parse_partitioning_info', - autospec=True) - @mock.patch.object(ansible_deploy, '_prepare_variables', autospec=True) - def test__deploy(self, prepare_vars_mock, parse_part_info_mock, - parse_dr_info_mock, prepare_extra_mock, - run_playbook_mock, set_boot_device_mock, - finish_deploy_mock, clean_ramdisk_mock): - ironic_nodes = { - 'ironic_nodes': [(self.node['uuid'], - DRIVER_INTERNAL_INFO['ansible_cleaning_ip'], - 'test_u')]} - prepare_extra_mock.return_value = ironic_nodes - _vars = { - 'url': 'image_url', - 'checksum': 'aa'} - prepare_vars_mock.return_value = _vars - - driver_internal_info = dict(DRIVER_INTERNAL_INFO) - driver_internal_info['is_whole_disk_image'] = False - self.node.driver_internal_info = driver_internal_info - self.node.extra = {'ham': 'spam'} - self.node.save() + @mock.patch.object(ansible_deploy.images, 'download_size', autospec=True) + def test__calculate_memory_req(self, image_mock): + self.config(group='ansible', extra_memory=1) + image_mock.return_value = 2000000 # < 2MiB with task_manager.acquire(self.context, self.node.uuid) as task: - ansible_deploy._deploy(task, '127.0.0.1') + self.assertEqual(2, ansible_deploy._calculate_memory_req(task)) + image_mock.assert_called_once_with(task.context, 'fake-image') - prepare_vars_mock.assert_called_once_with(task) - parse_part_info_mock.assert_called_once_with(task.node) - parse_dr_info_mock.assert_called_once_with(task.node) - prepare_extra_mock.assert_called_once_with( - [(self.node['uuid'], '127.0.0.1', 'test_u', {'ham': 'spam'})], - variables=_vars) - run_playbook_mock.assert_called_once_with( - 'test_pl', {'ironic_nodes': [ - (self.node['uuid'], - DRIVER_INTERNAL_INFO['ansible_cleaning_ip'], - 'test_u')]}, 'test_k', - notags=['wait']) - set_boot_device_mock.assert_called_once_with( - task, 'disk', persistent=True) - finish_deploy_mock.assert_called_once_with(task) - clean_ramdisk_mock.assert_called_once_with(task) + def test__get_configdrive_path(self): + self.config(tempdir='/path/to/tmpdir') + self.assertEqual('/path/to/tmpdir/spam.cndrive', + ansible_deploy._get_configdrive_path('spam')) - @mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk') - @mock.patch.object(ansible_deploy, '_reboot_and_finish_deploy', - autospec=True) - @mock.patch.object(utils, 'node_set_boot_device', autospec=True) - @mock.patch.object(ansible_deploy, '_run_playbook', autospec=True) - @mock.patch.object(ansible_deploy, '_prepare_extra_vars', autospec=True) - @mock.patch.object(ansible_deploy, '_parse_ansible_driver_info', - return_value=('test_pl', 'test_u', 'test_k'), - autospec=True) - @mock.patch.object(ansible_deploy, '_parse_partitioning_info', - autospec=True) - @mock.patch.object(ansible_deploy, '_prepare_variables', autospec=True) - def test__deploy_iwdi(self, prepare_vars_mock, parse_part_info_mock, - parse_dr_info_mock, prepare_extra_mock, - run_playbook_mock, set_boot_device_mock, - finish_deploy_mock, clean_ramdisk_mock): - ironic_nodes = { - 'ironic_nodes': [(self.node['uuid'], - DRIVER_INTERNAL_INFO['ansible_cleaning_ip'], - 'test_u')]} - prepare_extra_mock.return_value = ironic_nodes - _vars = { - 'url': 'image_url', - 'checksum': 'aa'} - prepare_vars_mock.return_value = _vars - driver_internal_info = self.node.driver_internal_info - driver_internal_info['is_whole_disk_image'] = True - self.node.driver_internal_info = driver_internal_info - self.node.extra = {'ham': 'spam'} - self.node.save() + def test__prepare_extra_vars(self): + host_list = [('fake-uuid', '1.2.3.4', 'spam', 'ham'), + ('other-uuid', '5.6.7.8', 'eggs', 'vikings')] + ansible_vars = {"foo": "bar"} + self.assertEqual( + {"ironic_nodes": [ + {"name": "fake-uuid", "ip": '1.2.3.4', + "user": "spam", "extra": "ham"}, + {"name": "other-uuid", "ip": '5.6.7.8', + "user": "eggs", "extra": "vikings"}], + "foo": "bar"}, + ansible_deploy._prepare_extra_vars(host_list, ansible_vars)) + @mock.patch.object(ansible_deploy, '_calculate_memory_req', autospec=True, + return_value=2000) + def test__prepare_variables(self, mem_req_mock): + expected = {"image": {"url": "http://image", "mem_req": 2000, + "disk_format": "qcow2", + "checksum": "md5:checksum"}} with task_manager.acquire(self.context, self.node.uuid) as task: - ansible_deploy._deploy(task, '127.0.0.1') + self.assertEqual(expected, + ansible_deploy._prepare_variables(task)) - prepare_vars_mock.assert_called_once_with(task) - self.assertFalse(parse_part_info_mock.called) - parse_dr_info_mock.assert_called_once_with(task.node) - prepare_extra_mock.assert_called_once_with( - [(self.node['uuid'], '127.0.0.1', 'test_u', {'ham': 'spam'})], - variables=_vars) - run_playbook_mock.assert_called_once_with( - 'test_pl', {'ironic_nodes': [ - (self.node['uuid'], - DRIVER_INTERNAL_INFO['ansible_cleaning_ip'], - 'test_u')]}, 'test_k', - notags=['wait', 'parted']) - set_boot_device_mock.assert_called_once_with( - task, 'disk', persistent=True) - finish_deploy_mock.assert_called_once_with(task) - clean_ramdisk_mock.assert_called_once_with(task) + @mock.patch.object(ansible_deploy, '_calculate_memory_req', autospec=True, + return_value=2000) + def test__prepare_variables_noglance(self, mem_req_mock): + i_info = self.node.instance_info + i_info['image_checksum'] = 'sha256:checksum' + self.node.instance_info = i_info + self.node.save() + expected = {"image": {"url": "http://image", "mem_req": 2000, + "disk_format": "qcow2", + "checksum": "sha256:checksum"}} + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertEqual(expected, + ansible_deploy._prepare_variables(task)) + + @mock.patch.object(ansible_deploy, '_calculate_memory_req', autospec=True, + return_value=2000) + def test__prepare_variables_configdrive_url(self, mem_req_mock): + i_info = self.node.instance_info + i_info['configdrive'] = 'http://configdrive_url' + self.node.instance_info = i_info + self.node.save() + expected = {"image": {"url": "http://image", "mem_req": 2000, + "disk_format": "qcow2", + "checksum": "md5:checksum"}, + 'configdrive': {'type': 'url', + 'location': 'http://configdrive_url'}} + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertEqual(expected, + ansible_deploy._prepare_variables(task)) + + @mock.patch.object(ansible_deploy, '_calculate_memory_req', autospec=True, + return_value=2000) + def test__prepare_variables_configdrive_file(self, mem_req_mock): + i_info = self.node.instance_info + i_info['configdrive'] = 'fake-content' + self.node.instance_info = i_info + self.node.save() + self.config(tempdir='/path/to/tmpfiles') + expected = {"image": {"url": "http://image", "mem_req": 2000, + "disk_format": "qcow2", + "checksum": "md5:checksum"}, + 'configdrive': {'type': 'file', + 'location': '/path/to/tmpfiles/%s.cndrive' + % self.node.uuid}} + with mock.patch.object(ansible_deploy, 'open', mock.mock_open(), + create=True) as open_mock: + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertEqual(expected, + ansible_deploy._prepare_variables(task)) + open_mock.assert_has_calls(( + mock.call('/path/to/tmpfiles/%s.cndrive' % self.node.uuid, + 'w'), + mock.call().__enter__(), + mock.call().write('fake-content'), + mock.call().__exit__(None, None, None))) def test__validate_clean_steps(self): steps = [{"interface": "deploy", @@ -453,8 +443,9 @@ class TestAnsibleDeploy(db_base.DbTestCase): self.node = object_utils.create_test_node(self.context, **node) def test_get_properties(self): - self.assertEqual(ansible_deploy.COMMON_PROPERTIES, - self.driver.get_properties()) + self.assertEqual( + set(ansible_deploy.COMMON_PROPERTIES), + set(self.driver.get_properties())) @mock.patch.object(deploy_utils, 'check_for_missing_params', autospec=True) @@ -495,19 +486,40 @@ class TestAnsibleDeploy(db_base.DbTestCase): self.assertEqual(driver_return, states.DEPLOYWAIT) power_mock.assert_called_once_with(task, states.REBOOT) - @mock.patch.object(ansible_deploy, '_deploy', autospec=True) @mock.patch.object(ansible_deploy, '_get_node_ip_dhcp', return_value='127.0.0.1', autospec=True) @mock.patch.object(utils, 'node_power_action', autospec=True) - def test_deploy_done(self, power_mock, get_ip_mock, deploy_mock): + def test_deploy_no_callback(self, power_mock, get_ip_mock): self.config(group='ansible', use_ramdisk_callback=False) - with task_manager.acquire( - self.context, self.node['uuid'], shared=False) as task: - driver_return = self.driver.deploy(task) - self.assertEqual(driver_return, states.DEPLOYDONE) - power_mock.assert_called_once_with(task, states.REBOOT) - get_ip_mock.assert_called_once_with(task) - deploy_mock.assert_called_once_with(task, '127.0.0.1') + with mock.patch.multiple(self.driver, + _ansible_deploy=mock.DEFAULT, + reboot_to_instance=mock.DEFAULT) as moks: + with task_manager.acquire( + self.context, self.node['uuid'], shared=False) as task: + driver_return = self.driver.deploy(task) + self.assertEqual(driver_return, states.DEPLOYDONE) + power_mock.assert_called_once_with(task, states.REBOOT) + get_ip_mock.assert_called_once_with(task) + moks['_ansible_deploy'].assert_called_once_with(task, + '127.0.0.1') + moks['reboot_to_instance'].assert_called_once_with(task) + + @mock.patch.object(deploy_utils, 'set_failed_state', autospec=True) + @mock.patch.object(ansible_deploy, '_get_node_ip_dhcp', + return_value='127.0.0.1', autospec=True) + @mock.patch.object(utils, 'node_power_action', autospec=True) + def test_deploy_no_callback_fail(self, power_mock, get_ip_mock, fail_mock): + self.config(group='ansible', use_ramdisk_callback=False) + with mock.patch.object(self.driver, '_ansible_deploy', + side_effect=ansible_deploy.PlaybookNotFound( + 'deploy')): + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + self.driver.deploy(task) + self.driver._ansible_deploy.assert_called_once_with( + task, '127.0.0.1') + fail_mock.assert_called_once_with(task, mock.ANY, + collect_logs=False) @mock.patch.object(utils, 'node_power_action', autospec=True) def test_tear_down(self, power_mock): @@ -739,101 +751,152 @@ class TestAnsibleDeploy(db_base.DbTestCase): (task.driver.network.remove_cleaning_network .assert_called_once_with(task)) - @mock.patch.object(ansible_deploy, 'LOG', autospec=True) - def test_heartbeat_not_wait_state(self, log_mock): - with task_manager.acquire(self.context, self.node.uuid) as task: - self.driver.heartbeat(task, 'http://127.0.0.1') - log_mock.warning.assert_called_once_with( - mock.ANY, {'node': task.node['uuid'], - 'state': task.node['provision_state']}) - - @mock.patch.object(ansible_deploy, 'LOG', autospec=True) - @mock.patch.object(ansible_deploy, '_deploy', autospec=True) - def test_heartbeat_deploy_wait(self, deploy_mock, log_mock): - self.node['provision_state'] = states.DEPLOYWAIT - self.node.save() - - with task_manager.acquire(self.context, self.node.uuid) as task: - task.process_event = mock.Mock() - - self.driver.heartbeat(task, 'http://127.0.0.1') - - deploy_mock.assert_called_once_with(task, '127.0.0.1') - log_mock.info.assert_called_once_with(mock.ANY, task.node['uuid']) - self.assertEqual([mock.call('resume'), mock.call('done')], - task.process_event.mock_calls) - - @mock.patch.object(deploy_utils, 'set_failed_state', autospec=True) - @mock.patch.object(ansible_deploy, 'LOG', autospec=True) - @mock.patch.object(ansible_deploy, '_deploy', - side_effect=Exception('Boo'), autospec=True) - def test_heartbeat_deploy_wait_fail(self, deploy_mock, log_mock, - set_fail_state_mock): - self.node['provision_state'] = states.DEPLOYWAIT - self.node.save() - - with task_manager.acquire(self.context, self.node.uuid) as task: - task.process_event = mock.Mock() - - self.driver.heartbeat(task, 'http://127.0.0.1') - - deploy_mock.assert_called_once_with(task, '127.0.0.1') - log_mock.exception.assert_called_once_with(mock.ANY) - self.assertEqual([mock.call('resume')], - task.process_event.mock_calls) - set_fail_state_mock.assert_called_once_with(task, mock.ANY, - collect_logs=False) - - @mock.patch.object(ansible_deploy, '_notify_conductor_resume_clean', + @mock.patch.object(ansible_deploy, '_run_playbook', autospec=True) + @mock.patch.object(ansible_deploy, '_prepare_extra_vars', autospec=True) + @mock.patch.object(ansible_deploy, '_parse_ansible_driver_info', + return_value=('test_pl', 'test_u', 'test_k'), autospec=True) - def test_heartbeat_clean_wait(self, notify_resume_clean_mock): - self.node['provision_state'] = states.CLEANWAIT - self.node.save() - - with task_manager.acquire(self.context, self.node.uuid) as task: - task.process_event = mock.Mock() - - self.driver.heartbeat(task, 'http://127.0.0.1') - - notify_resume_clean_mock.assert_called_once_with(task) - - @mock.patch.object(ansible_deploy, '_notify_conductor_resume_clean', - side_effect=Exception('Boo'), autospec=True) - @mock.patch.object(utils, 'cleaning_error_handler', autospec=True) - def test_heartbeat_clean_wait_fail(self, cleaning_error_mock, - notify_resume_clean_mock): - self.node['provision_state'] = states.CLEANWAIT - self.node.save() - - with task_manager.acquire(self.context, self.node.uuid) as task: - task.process_event = mock.Mock() - - self.driver.heartbeat(task, 'http://127.0.0.1') - - notify_resume_clean_mock.assert_called_once_with(task) - cleaning_error_mock.assert_called_once_with(task, mock.ANY) - - @mock.patch.object(ansible_deploy, '_notify_conductor_resume_clean', + @mock.patch.object(ansible_deploy, '_parse_partitioning_info', autospec=True) - @mock.patch.object(ansible_deploy, '_deploy', autospec=True) - @mock.patch.object(ansible_deploy, 'LOG', autospec=True) - def test_heartbeat_maintenance(self, log_mock, deploy_mock, - notify_clean_resume_mock): - self.node['maintenance'] = True + @mock.patch.object(ansible_deploy, '_prepare_variables', autospec=True) + def test__ansible_deploy(self, prepare_vars_mock, parse_part_info_mock, + parse_dr_info_mock, prepare_extra_mock, + run_playbook_mock): + ironic_nodes = { + 'ironic_nodes': [(self.node['uuid'], + DRIVER_INTERNAL_INFO['ansible_cleaning_ip'], + 'test_u')]} + prepare_extra_mock.return_value = ironic_nodes + _vars = { + 'url': 'image_url', + 'checksum': 'aa'} + prepare_vars_mock.return_value = _vars + + driver_internal_info = dict(DRIVER_INTERNAL_INFO) + driver_internal_info['is_whole_disk_image'] = False + self.node.driver_internal_info = driver_internal_info + self.node.extra = {'ham': 'spam'} + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.driver._ansible_deploy(task, '127.0.0.1') + + prepare_vars_mock.assert_called_once_with(task) + parse_part_info_mock.assert_called_once_with(task.node) + parse_dr_info_mock.assert_called_once_with(task.node) + prepare_extra_mock.assert_called_once_with( + [(self.node['uuid'], '127.0.0.1', 'test_u', {'ham': 'spam'})], + variables=_vars) + run_playbook_mock.assert_called_once_with( + 'test_pl', {'ironic_nodes': [ + (self.node['uuid'], + DRIVER_INTERNAL_INFO['ansible_cleaning_ip'], + 'test_u')]}, 'test_k', + notags=['wait']) + + @mock.patch.object(ansible_deploy, '_run_playbook', autospec=True) + @mock.patch.object(ansible_deploy, '_prepare_extra_vars', autospec=True) + @mock.patch.object(ansible_deploy, '_parse_ansible_driver_info', + return_value=('test_pl', 'test_u', 'test_k'), + autospec=True) + @mock.patch.object(ansible_deploy, '_parse_partitioning_info', + autospec=True) + @mock.patch.object(ansible_deploy, '_prepare_variables', autospec=True) + def test__ansible_deploy_iwdi(self, prepare_vars_mock, + parse_part_info_mock, parse_dr_info_mock, + prepare_extra_mock, run_playbook_mock): + ironic_nodes = { + 'ironic_nodes': [(self.node['uuid'], + DRIVER_INTERNAL_INFO['ansible_cleaning_ip'], + 'test_u')]} + prepare_extra_mock.return_value = ironic_nodes + _vars = { + 'url': 'image_url', + 'checksum': 'aa'} + prepare_vars_mock.return_value = _vars + driver_internal_info = self.node.driver_internal_info + driver_internal_info['is_whole_disk_image'] = True + self.node.driver_internal_info = driver_internal_info + self.node.extra = {'ham': 'spam'} + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.driver._ansible_deploy(task, '127.0.0.1') + + prepare_vars_mock.assert_called_once_with(task) + self.assertFalse(parse_part_info_mock.called) + parse_dr_info_mock.assert_called_once_with(task.node) + prepare_extra_mock.assert_called_once_with( + [(self.node['uuid'], '127.0.0.1', 'test_u', {'ham': 'spam'})], + variables=_vars) + run_playbook_mock.assert_called_once_with( + 'test_pl', {'ironic_nodes': [ + (self.node['uuid'], + DRIVER_INTERNAL_INFO['ansible_cleaning_ip'], + 'test_u')]}, 'test_k', + notags=['wait', 'parted']) + + @mock.patch.object(utils, 'node_power_action', autospec=True) + @mock.patch.object(fake.FakePower, 'get_power_state', + return_value=states.POWER_ON) + def test_reboot_and_finish_deploy_soft_poweroff_retry(self, + get_pow_state_mock, + power_action_mock): + self.config(group='ansible', + post_deploy_get_power_state_retry_interval=0) + self.config(group='ansible', + post_deploy_get_power_state_retries=1) + self.node.provision_state = states.DEPLOYING + di_info = self.node.driver_internal_info + di_info['agent_url'] = 'http://127.0.0.1' + self.node.driver_internal_info = di_info + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid) as task: + with mock.patch.object(task.driver, 'network') as net_mock: + self.driver.reboot_and_finish_deploy(task) + net_mock.remove_provisioning_network.assert_called_once_with( + task) + net_mock.configure_tenant_networks.assert_called_once_with( + task) + power_action_mock.assert_has_calls( + [mock.call(task, states.POWER_OFF), + mock.call(task, states.POWER_ON)]) + get_pow_state_mock.assert_called_with(task) + self.assertEqual(2, len(get_pow_state_mock.mock_calls)) + expected_power_calls = [((task, states.POWER_OFF),), + ((task, states.POWER_ON),)] + self.assertEqual(expected_power_calls, + power_action_mock.call_args_list) + + @mock.patch.object(ansible_deploy, '_get_node_ip_heartbeat', autospec=True, + return_value='1.2.3.4') + def test_continue_deploy(self, getip_mock): + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE self.node.save() with task_manager.acquire(self.context, self.node.uuid) as task: - self.driver.heartbeat(task, 'http://127.0.0.1') + with mock.patch.multiple(self.driver, autospec=True, + _ansible_deploy=mock.DEFAULT, + reboot_to_instance=mock.DEFAULT): + self.driver.continue_deploy(task) + getip_mock.assert_called_once_with(task) + self.driver._ansible_deploy.assert_called_once_with( + task, '1.2.3.4') + self.driver.reboot_to_instance.assert_called_once_with(task) + self.assertEqual(states.ACTIVE, task.node.target_provision_state) + self.assertEqual(states.DEPLOYING, task.node.provision_state) - self.node['provision_state'] = states.CLEANWAIT - self.node.save() + @mock.patch.object(utils, 'node_set_boot_device', autospec=True) + def test_reboot_to_instance(self, bootdev_mock): with task_manager.acquire(self.context, self.node.uuid) as task: - self.driver.heartbeat(task, 'http://127.0.0.1') - - self.node['provision_state'] = states.DEPLOYWAIT - self.node.save() - with task_manager.acquire(self.context, self.node.uuid) as task: - self.driver.heartbeat(task, 'http://127.0.0.1') - - self.assertFalse(log_mock.warning.called) - self.assertFalse(deploy_mock.called) - self.assertFalse(notify_clean_resume_mock.called) + with mock.patch.object(self.driver, 'reboot_and_finish_deploy', + autospec=True): + task.driver.boot = mock.Mock() + self.driver.reboot_to_instance(task) + bootdev_mock.assert_called_once_with(task, 'disk', + persistent=True) + self.driver.reboot_and_finish_deploy.assert_called_once_with( + task) + task.driver.boot.clean_up_ramdisk.assert_called_once_with( + task) diff --git a/releasenotes/notes/ansible-new-ironic-12e74f80cd72dbe3.yaml b/releasenotes/notes/ansible-new-ironic-12e74f80cd72dbe3.yaml index a6d92be..a350a7d 100644 --- a/releasenotes/notes/ansible-new-ironic-12e74f80cd72dbe3.yaml +++ b/releasenotes/notes/ansible-new-ironic-12e74f80cd72dbe3.yaml @@ -1,3 +1,3 @@ --- upgrade: - - Ansible-deploy driver requires ironic of version >= 7.0.0 (Ocata release). + - Ansible-deploy driver requires ironic of version >= 8.0.0 (Pike release)