[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
This commit is contained in:
Pavlo Shchelokovskyy 2017-03-17 15:37:07 +00:00
parent 77326e7a5c
commit 6e8cd3163f
4 changed files with 404 additions and 370 deletions

View File

@ -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)

View File

@ -15,3 +15,6 @@
- include: grub.yaml
tags:
- parted
- name: flush
command: sync

View File

@ -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)

View File

@ -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)