diff --git a/doc/source/admin/drivers/ansible.rst b/doc/source/admin/drivers/ansible.rst index 48ba9883a1..147b009b0e 100644 --- a/doc/source/admin/drivers/ansible.rst +++ b/doc/source/admin/drivers/ansible.rst @@ -231,6 +231,13 @@ ansible_clean_steps_config Default is taken from ``[ansible]/default_clean_steps_config`` option of the ironic configuration file (defaults to ``clean_steps.yaml``). +ansible_python_interpreter + Absolute path to the python interpreter on the managed machine. + Default is taken from ``[ansible]/default_python_interpreter`` option of + the ironic configuration file. + Ansible uses ``/usr/bin/python`` by default. + + Customizing the deployment logic ================================ diff --git a/ironic/conf/ansible.py b/ironic/conf/ansible.py index bcabb1b963..839fdca413 100644 --- a/ironic/conf/ansible.py +++ b/ironic/conf/ansible.py @@ -136,8 +136,10 @@ opts = [ "'driver_info' field.")), cfg.StrOpt('default_python_interpreter', help=_("Absolute path to the python interpreter on the " - "managed machines. By default, ansible uses " - "/usr/bin/python")), + "managed machines. It may be overridden by per-node " + "'ansible_python_interpreter' option in node's " + "'driver_info' field. " + "By default, ansible uses /usr/bin/python")), ] diff --git a/ironic/drivers/modules/ansible/deploy.py b/ironic/drivers/modules/ansible/deploy.py index 17d7174520..3732695be2 100644 --- a/ironic/drivers/modules/ansible/deploy.py +++ b/ironic/drivers/modules/ansible/deploy.py @@ -73,6 +73,8 @@ OPTIONAL_PROPERTIES = { 'ansible_clean_steps_config': _('Name of the file inside the ' '"ansible_playbooks_path" folder with ' 'cleaning steps configuration. Optional.'), + 'ansible_python_interpreter': _('Absolute path to the python interpreter ' + 'on the managed machines. Optional.'), } COMMON_PROPERTIES = OPTIONAL_PROPERTIES @@ -101,6 +103,11 @@ def _parse_ansible_driver_info(node, action='deploy'): return os.path.basename(playbook), user, key +def _get_python_interpreter(node): + return node.driver_info.get('ansible_python_interpreter', + CONF.ansible.default_python_interpreter) + + def _get_configdrive_path(basename): return os.path.join(CONF.tempdir, basename + '.cndrive') @@ -126,9 +133,9 @@ def _run_playbook(node, name, extra_vars, key, tags=None, notags=None): playbook = os.path.join(root, name) inventory = os.path.join(root, 'inventory') ironic_vars = {'ironic': extra_vars} - if CONF.ansible.default_python_interpreter: - ironic_vars['ansible_python_interpreter'] = ( - CONF.ansible.default_python_interpreter) + python_interpreter = _get_python_interpreter(node) + if python_interpreter: + ironic_vars['ansible_python_interpreter'] = python_interpreter args = [CONF.ansible.ansible_playbook_script, playbook, '-i', inventory, '-e', json.dumps(ironic_vars), diff --git a/ironic/tests/unit/drivers/modules/ansible/test_deploy.py b/ironic/tests/unit/drivers/modules/ansible/test_deploy.py index c9f0e455f4..d4a8da6179 100644 --- a/ironic/tests/unit/drivers/modules/ansible/test_deploy.py +++ b/ironic/tests/unit/drivers/modules/ansible/test_deploy.py @@ -191,6 +191,35 @@ class TestAnsibleMethods(AnsibleDeployTestCaseBase): "ironic": {"foo": "bar"}}, json.loads(all_vars)) + @mock.patch.object(com_utils, 'execute', return_value=('out', 'err'), + autospec=True) + def test__run_playbook_ansible_interpreter_override(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', + default_python_interpreter='/usr/bin/python3') + self.config(group='ansible', ansible_extra_args='--timeout=100') + self.node.driver_info['ansible_python_interpreter'] = ( + '/usr/bin/python4') + extra_vars = {'foo': 'bar'} + + ansible_deploy._run_playbook(self.node, 'deploy', + extra_vars, '/path/to/key', + tags=['spam'], notags=['ham']) + + execute_mock.assert_called_once_with( + 'env', 'ANSIBLE_CONFIG=/path/to/config', + 'ansible-playbook', '/path/to/playbooks/deploy', '-i', + '/path/to/playbooks/inventory', '-e', + mock.ANY, '--tags=spam', '--skip-tags=ham', + '--private-key=/path/to/key', '-vvv', '--timeout=100') + + all_vars = execute_mock.call_args[0][7] + self.assertEqual({"ansible_python_interpreter": "/usr/bin/python4", + "ironic": {"foo": "bar"}}, + json.loads(all_vars)) + @mock.patch.object(com_utils, 'execute', side_effect=processutils.ProcessExecutionError( description='VIKINGS!'), @@ -286,6 +315,16 @@ class TestAnsibleMethods(AnsibleDeployTestCaseBase): self.assertEqual(2, ansible_deploy._calculate_memory_req(task)) image_mock.assert_called_once_with(task.context, 'fake-image') + def test__get_python_interpreter(self): + self.config(group='ansible', + default_python_interpreter='/usr/bin/python3') + self.node.driver_info['ansible_python_interpreter'] = ( + '/usr/bin/python4') + + python_interpreter = ansible_deploy._get_python_interpreter(self.node) + + self.assertEqual('/usr/bin/python4', python_interpreter) + def test__get_configdrive_path(self): self.config(tempdir='/path/to/tmpdir') self.assertEqual('/path/to/tmpdir/spam.cndrive', diff --git a/releasenotes/notes/add-ansible-python-interpreter-2035e0f23d407aaf.yaml b/releasenotes/notes/add-ansible-python-interpreter-2035e0f23d407aaf.yaml index 9981967598..b653ec2592 100644 --- a/releasenotes/notes/add-ansible-python-interpreter-2035e0f23d407aaf.yaml +++ b/releasenotes/notes/add-ansible-python-interpreter-2035e0f23d407aaf.yaml @@ -11,3 +11,5 @@ features: ``/usr/bin/python3``. The same interpreter will be used in all operations that use the ansible deploy interface. + It is also possible to override the value set in the configuration for a + node by passing ``ansible_python_interpreter`` in its ``driver_info``. \ No newline at end of file