From d913ef639691f1811775e4493985339c3421203e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aija=20Jaunt=C4=93va?= Date: Mon, 29 Mar 2021 06:51:49 -0400 Subject: [PATCH] Move configuration mold utilities Utilities moved to ironic.common.molds. New config section [molds] created and settings moved there. Change-Id: I1177f7dd5d5157bb3a5c0bd09acd75c9a394ab47 --- ironic/common/molds.py | 112 +++++++++ ironic/conf/__init__.py | 2 + ironic/conf/default.py | 21 -- ironic/conf/molds.py | 41 ++++ ironic/conf/opts.py | 1 + ironic/drivers/utils.py | 85 ------- ironic/tests/unit/common/test_molds.py | 291 ++++++++++++++++++++++++ ironic/tests/unit/drivers/test_utils.py | 268 ---------------------- 8 files changed, 447 insertions(+), 374 deletions(-) create mode 100644 ironic/common/molds.py create mode 100644 ironic/conf/molds.py create mode 100644 ironic/tests/unit/common/test_molds.py diff --git a/ironic/common/molds.py b/ironic/common/molds.py new file mode 100644 index 0000000000..9c6a439bd5 --- /dev/null +++ b/ironic/common/molds.py @@ -0,0 +1,112 @@ +# Copyright (c) 2021 Dell Inc. or its subsidiaries. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_serialization import base64 +import requests +import tenacity + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.common import swift + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF + + +def save_configuration(task, url, data): + """Store configuration mold to indicated location. + + :param task: A TaskManager instance. + :param name: URL of the configuration item to save to. + :param data: Content of JSON data to save. + + :raises IronicException: If using Swift storage and no authentication + token found in task's context. + :raises HTTPError: If failed to complete HTTP request. + """ + @tenacity.retry( + retry=tenacity.retry_if_exception_type( + requests.exceptions.ConnectionError), + stop=tenacity.stop_after_attempt(CONF.molds.retry_attempts), + wait=tenacity.wait_fixed(CONF.molds.retry_interval), + reraise=True + ) + def _request(url, data, auth_header): + return requests.put( + url, data=json.dumps(data, indent=2), headers=auth_header) + + auth_header = _get_auth_header(task) + response = _request(url, data, auth_header) + response.raise_for_status() + + +def get_configuration(task, url): + """Gets configuration mold from indicated location. + + :param task: A TaskManager instance. + :param url: URL of the configuration item to get. + + :returns: JSON configuration mold + + :raises IronicException: If using Swift storage and no authentication + token found in task's context. + :raises HTTPError: If failed to complete HTTP request. + """ + @tenacity.retry( + retry=tenacity.retry_if_exception_type( + requests.exceptions.ConnectionError), + stop=tenacity.stop_after_attempt(CONF.molds.retry_attempts), + wait=tenacity.wait_fixed(CONF.molds.retry_interval), + reraise=True + ) + def _request(url, auth_header): + return requests.get(url, headers=auth_header) + + auth_header = _get_auth_header(task) + response = _request(url, auth_header) + if response.status_code == requests.codes.ok: + return response.json() + + response.raise_for_status() + + +def _get_auth_header(task): + """Based on setup of configuration mold storage gets authentication header + + :param task: A TaskManager instance. + :raises IronicException: If using Swift storage and no authentication + token found in task's context. + """ + auth_header = None + if CONF.molds.storage == 'swift': + # TODO(ajya) Need to update to use Swift client and context session + auth_token = swift.get_swift_session().get_token() + if auth_token: + auth_header = {'X-Auth-Token': auth_token} + else: + raise exception.IronicException( + _('Missing auth_token for configuration mold access for node ' + '%s') % task.node.uuid) + elif CONF.molds.storage == 'http': + if CONF.molds.user and CONF.molds.password: + auth_header = {'Authorization': 'Basic %s' + % base64.encode_as_text( + '%s:%s' % (CONF.molds.user, + CONF.molds.password))} + return auth_header diff --git a/ironic/conf/__init__.py b/ironic/conf/__init__.py index a1da1fb7e2..2d6fab8db0 100644 --- a/ironic/conf/__init__.py +++ b/ironic/conf/__init__.py @@ -38,6 +38,7 @@ from ironic.conf import irmc from ironic.conf import iscsi from ironic.conf import metrics from ironic.conf import metrics_statsd +from ironic.conf import molds from ironic.conf import neutron from ironic.conf import nova from ironic.conf import pxe @@ -72,6 +73,7 @@ iscsi.register_opts(CONF) anaconda.register_opts(CONF) metrics.register_opts(CONF) metrics_statsd.register_opts(CONF) +molds.register_opts(CONF) neutron.register_opts(CONF) nova.register_opts(CONF) pxe.register_opts(CONF) diff --git a/ironic/conf/default.py b/ironic/conf/default.py index 0c41f73d05..bb5ec39180 100644 --- a/ironic/conf/default.py +++ b/ironic/conf/default.py @@ -423,26 +423,6 @@ webserver_opts = [ 'with images.')), ] -configuration_mold_opts = [ - cfg.StrOpt('mold_storage', - default='swift', - help=_('Configuration mold storage location. Supports "swift" ' - 'and "http". By default "swift".')), - cfg.StrOpt('mold_user', - help=_('User for "http" Basic auth. By default set empty.')), - cfg.StrOpt('mold_password', - help=_('Password for "http" Basic auth. By default set ' - 'empty.')), - cfg.StrOpt('mold_retry_attempts', - default=3, - help=_('Retry attempts for saving or getting configuration ' - 'molds.')), - cfg.StrOpt('mold_retry_interval', - default=3, - help=_('Retry interval for saving or getting configuration ' - 'molds.')) -] - def register_opts(conf): conf.register_opts(api_opts) @@ -458,4 +438,3 @@ def register_opts(conf): conf.register_opts(service_opts) conf.register_opts(utils_opts) conf.register_opts(webserver_opts) - conf.register_opts(configuration_mold_opts) diff --git a/ironic/conf/molds.py b/ironic/conf/molds.py new file mode 100644 index 0000000000..4cec1749a4 --- /dev/null +++ b/ironic/conf/molds.py @@ -0,0 +1,41 @@ +# Copyright (c) 2021 Dell Inc. or its subsidiaries. +# +# 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. + +from oslo_config import cfg + +from ironic.common.i18n import _ + +opts = [ + cfg.StrOpt('storage', + default='swift', + help=_('Configuration mold storage location. Supports "swift" ' + 'and "http". By default "swift".')), + cfg.StrOpt('user', + help=_('User for "http" Basic auth. By default set empty.')), + cfg.StrOpt('password', + help=_('Password for "http" Basic auth. By default set ' + 'empty.')), + cfg.StrOpt('retry_attempts', + default=3, + help=_('Retry attempts for saving or getting configuration ' + 'molds.')), + cfg.StrOpt('retry_interval', + default=3, + help=_('Retry interval for saving or getting configuration ' + 'molds.')) +] + + +def register_opts(conf): + conf.register_opts(opts, group='molds') diff --git a/ironic/conf/opts.py b/ironic/conf/opts.py index 976611be66..eb61adf88d 100644 --- a/ironic/conf/opts.py +++ b/ironic/conf/opts.py @@ -54,6 +54,7 @@ _opts = [ ('anaconda', ironic.conf.anaconda.opts), ('metrics', ironic.conf.metrics.opts), ('metrics_statsd', ironic.conf.metrics_statsd.opts), + ('molds', ironic.conf.molds.opts), ('neutron', ironic.conf.neutron.list_opts()), ('nova', ironic.conf.nova.list_opts()), ('pxe', ironic.conf.pxe.opts), diff --git a/ironic/drivers/utils.py b/ironic/drivers/utils.py index 9e8a1611ed..4aa2335d67 100644 --- a/ironic/drivers/utils.py +++ b/ironic/drivers/utils.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import json import os import tempfile @@ -21,8 +20,6 @@ from oslo_log import log as logging from oslo_serialization import base64 from oslo_utils import strutils from oslo_utils import timeutils -import requests -import tenacity from ironic.common import exception from ironic.common.i18n import _ @@ -387,85 +384,3 @@ OPTIONAL_PROPERTIES = { "deprecated in favor of the new ones." "Defaults to 'Default'. Optional."), } - - -def save_configuration_mold(task, url, data): - """Store configuration mold to indicated location. - - :param task: A TaskManager instance. - :param name: URL of the configuration item to save to. - :param data: Content of JSON data to save. - - :raises IronicException: If using Swift storage and no authentication - token found in task's context. - :raises HTTPError: If failed to complete HTTP request. - """ - @tenacity.retry( - retry=tenacity.retry_if_exception_type( - requests.exceptions.ConnectionError), - stop=tenacity.stop_after_attempt(CONF.mold_retry_attempts), - wait=tenacity.wait_fixed(CONF.mold_retry_interval), - reraise=True - ) - def _request(url, data, auth_header): - return requests.put( - url, data=json.dumps(data, indent=2), headers=auth_header) - - auth_header = _get_auth_header(task) - response = _request(url, data, auth_header) - response.raise_for_status() - - -def get_configuration_mold(task, url): - """Gets configuration mold from indicated location. - - :param task: A TaskManager instance. - :param url: URL of the configuration item to get. - - :returns: JSON configuration mold - - :raises IronicException: If using Swift storage and no authentication - token found in task's context. - :raises HTTPError: If failed to complete HTTP request. - """ - @tenacity.retry( - retry=tenacity.retry_if_exception_type( - requests.exceptions.ConnectionError), - stop=tenacity.stop_after_attempt(CONF.mold_retry_attempts), - wait=tenacity.wait_fixed(CONF.mold_retry_interval), - reraise=True - ) - def _request(url, auth_header): - return requests.get(url, headers=auth_header) - - auth_header = _get_auth_header(task) - response = _request(url, auth_header) - if response.status_code == requests.codes.ok: - return response.json() - - response.raise_for_status() - - -def _get_auth_header(task): - """Based on setup of configuration mold storage gets authentication header - - :param task: A TaskManager instance. - :raises IronicException: If using Swift storage and no authentication - token found in task's context. - """ - auth_header = None - if CONF.mold_storage == 'swift': - # TODO(ajya) Need to update to use Swift client and context session - auth_token = swift.get_swift_session().get_token() - if auth_token: - auth_header = {'X-Auth-Token': auth_token} - else: - raise exception.IronicException( - _('Missing auth_token for configuration mold access for node ' - '%s') % task.node.uuid) - elif CONF.mold_storage == 'http': - if CONF.mold_user and CONF.mold_password: - auth_header = {'Authorization': 'Basic %s' - % base64.encode_as_text( - '%s:%s' % (CONF.mold_user, CONF.mold_password))} - return auth_header diff --git a/ironic/tests/unit/common/test_molds.py b/ironic/tests/unit/common/test_molds.py new file mode 100644 index 0000000000..53c0ad0acd --- /dev/null +++ b/ironic/tests/unit/common/test_molds.py @@ -0,0 +1,291 @@ +# Copyright (c) 2021 Dell Inc. or its subsidiaries. +# +# 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. + +from unittest import mock + +from oslo_config import cfg +import requests + +from ironic.common import exception +from ironic.common import molds +from ironic.common import swift +from ironic.conductor import task_manager +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.objects import utils as obj_utils + + +class ConfigurationMoldTestCase(db_base.DbTestCase): + + def setUp(self): + super(ConfigurationMoldTestCase, self).setUp() + self.node = obj_utils.create_test_node(self.context) + + @mock.patch.object(swift, 'get_swift_session', autospec=True) + @mock.patch.object(requests, 'put', autospec=True) + def test_save_configuration_swift(self, mock_put, mock_swift): + mock_session = mock.Mock() + mock_session.get_token.return_value = 'token' + mock_swift.return_value = mock_session + cfg.CONF.molds.storage = 'swift' + url = 'https://example.com/file1' + data = {'key': 'value'} + + with task_manager.acquire(self.context, self.node.uuid) as task: + molds.save_configuration(task, url, data) + + mock_put.assert_called_once_with(url, '{\n "key": "value"\n}', + headers={'X-Auth-Token': 'token'}) + + @mock.patch.object(swift, 'get_swift_session', autospec=True) + @mock.patch.object(requests, 'put', autospec=True) + def test_save_configuration_swift_noauth(self, mock_put, mock_swift): + mock_session = mock.Mock() + mock_session.get_token.return_value = None + mock_swift.return_value = mock_session + cfg.CONF.molds.storage = 'swift' + url = 'https://example.com/file1' + data = {'key': 'value'} + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises( + exception.IronicException, + molds.save_configuration, + task, url, data) + + @mock.patch.object(requests, 'put', autospec=True) + def test_save_configuration_http(self, mock_put): + cfg.CONF.molds.storage = 'http' + cfg.CONF.molds.user = 'user' + cfg.CONF.molds.password = 'password' + url = 'https://example.com/file1' + data = {'key': 'value'} + + with task_manager.acquire(self.context, self.node.uuid) as task: + molds.save_configuration(task, url, data) + + mock_put.assert_called_once_with( + url, '{\n "key": "value"\n}', + headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) + + @mock.patch.object(requests, 'put', autospec=True) + def test_save_configuration_http_noauth(self, mock_put): + cfg.CONF.molds.storage = 'http' + cfg.CONF.molds.user = None + cfg.CONF.molds.password = None + url = 'https://example.com/file1' + data = {'key': 'value'} + + with task_manager.acquire(self.context, self.node.uuid) as task: + molds.save_configuration(task, url, data) + mock_put.assert_called_once_with( + url, '{\n "key": "value"\n}', + headers=None) + + @mock.patch.object(requests, 'put', autospec=True) + def test_save_configuration_http_error(self, mock_put): + cfg.CONF.molds.storage = 'http' + cfg.CONF.molds.user = 'user' + cfg.CONF.molds.password = 'password' + response = mock.MagicMock() + response.status_code = 404 + response.raise_for_status.side_effect = requests.exceptions.HTTPError + mock_put.return_value = response + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises( + requests.exceptions.HTTPError, + molds.save_configuration, + task, + 'https://example.com/file2', + {'key': 'value'}) + mock_put.assert_called_once_with( + 'https://example.com/file2', '{\n "key": "value"\n}', + headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) + + @mock.patch.object(requests, 'put', autospec=True) + def test_save_configuration_connection_error(self, mock_put): + cfg.CONF.molds.storage = 'http' + cfg.CONF.molds.user = 'user' + cfg.CONF.molds.password = 'password' + cfg.CONF.molds.retry_interval = 0 + cfg.CONF.molds.retry_attempts = 3 + response = mock.MagicMock() + mock_put.side_effect = [ + requests.exceptions.ConnectTimeout, + requests.exceptions.ConnectionError, + response] + + with task_manager.acquire(self.context, self.node.uuid) as task: + molds.save_configuration( + task, 'https://example.com/file2', {'key': 'value'}) + mock_put.assert_called_with( + 'https://example.com/file2', '{\n "key": "value"\n}', + headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) + self.assertEqual(mock_put.call_count, 3) + + @mock.patch.object(requests, 'put', autospec=True) + def test_save_configuration_connection_error_exceeded(self, mock_put): + cfg.CONF.molds.storage = 'http' + cfg.CONF.molds.user = 'user' + cfg.CONF.molds.password = 'password' + cfg.CONF.molds.retry_interval = 0 + cfg.CONF.molds.retry_attempts = 2 + mock_put.side_effect = [ + requests.exceptions.ConnectTimeout, + requests.exceptions.ConnectionError] + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises( + requests.exceptions.ConnectionError, + molds.save_configuration, + task, + 'https://example.com/file2', + {'key': 'value'}) + mock_put.assert_called_with( + 'https://example.com/file2', '{\n "key": "value"\n}', + headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) + self.assertEqual(mock_put.call_count, 2) + + @mock.patch.object(swift, 'get_swift_session', autospec=True) + @mock.patch.object(requests, 'get', autospec=True) + def test_get_configuration_swift(self, mock_get, mock_swift): + mock_session = mock.Mock() + mock_session.get_token.return_value = 'token' + mock_swift.return_value = mock_session + cfg.CONF.molds.storage = 'swift' + response = mock.MagicMock() + response.status_code = 200 + response.json.return_value = {'key': 'value'} + mock_get.return_value = response + url = 'https://example.com/file1' + + with task_manager.acquire(self.context, self.node.uuid) as task: + result = molds.get_configuration(task, url) + + mock_get.assert_called_once_with( + url, headers={'X-Auth-Token': 'token'}) + self.assertJsonEqual({'key': 'value'}, result) + + @mock.patch.object(swift, 'get_swift_session', autospec=True) + @mock.patch.object(requests, 'get', autospec=True) + def test_get_configuration_swift_noauth(self, mock_get, mock_swift): + mock_session = mock.Mock() + mock_session.get_token.return_value = None + mock_swift.return_value = mock_session + cfg.CONF.molds.storage = 'swift' + url = 'https://example.com/file1' + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises( + exception.IronicException, + molds.get_configuration, + task, url) + + @mock.patch.object(requests, 'get', autospec=True) + def test_get_configuration_http(self, mock_get): + cfg.CONF.molds.storage = 'http' + cfg.CONF.molds.user = 'user' + cfg.CONF.molds.password = 'password' + response = mock.MagicMock() + response.status_code = 200 + response.json.return_value = {'key': 'value'} + mock_get.return_value = response + url = 'https://example.com/file2' + + with task_manager.acquire(self.context, self.node.uuid) as task: + result = molds.get_configuration(task, url) + + mock_get.assert_called_once_with( + url, headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) + self.assertJsonEqual({"key": "value"}, result) + + @mock.patch.object(requests, 'get', autospec=True) + def test_get_configuration_http_noauth(self, mock_get): + cfg.CONF.molds.storage = 'http' + cfg.CONF.molds.user = None + cfg.CONF.molds.password = None + response = mock.MagicMock() + response.status_code = 200 + response.json.return_value = {'key': 'value'} + mock_get.return_value = response + url = 'https://example.com/file2' + + with task_manager.acquire(self.context, self.node.uuid) as task: + result = molds.get_configuration(task, url) + + mock_get.assert_called_once_with(url, headers=None) + self.assertJsonEqual({"key": "value"}, result) + + @mock.patch.object(requests, 'get', autospec=True) + def test_get_configuration_http_error(self, mock_get): + cfg.CONF.molds.storage = 'http' + cfg.CONF.molds.user = 'user' + cfg.CONF.molds.password = 'password' + response = mock.MagicMock() + response.status_code = 404 + response.raise_for_status.side_effect = requests.exceptions.HTTPError + mock_get.return_value = response + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises( + requests.exceptions.HTTPError, + molds.get_configuration, + task, + 'https://example.com/file2') + mock_get.assert_called_once_with( + 'https://example.com/file2', + headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) + + @mock.patch.object(requests, 'get', autospec=True) + def test_get_configuration_connection_error(self, mock_get): + cfg.CONF.molds.storage = 'http' + cfg.CONF.molds.user = 'user' + cfg.CONF.molds.password = 'password' + cfg.CONF.molds.retry_interval = 0 + cfg.CONF.molds.retry_attempts = 3 + response = mock.MagicMock() + mock_get.side_effect = [ + requests.exceptions.ConnectTimeout, + requests.exceptions.ConnectionError, + response] + + with task_manager.acquire(self.context, self.node.uuid) as task: + molds.get_configuration( + task, 'https://example.com/file2') + mock_get.assert_called_with( + 'https://example.com/file2', + headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) + self.assertEqual(mock_get.call_count, 3) + + @mock.patch.object(requests, 'get', autospec=True) + def test_get_configuration_mold_connection_error_exceeded(self, mock_get): + cfg.CONF.molds.storage = 'http' + cfg.CONF.molds.user = 'user' + cfg.CONF.molds.password = 'password' + cfg.CONF.molds.retry_interval = 0 + cfg.CONF.molds.retry_attempts = 2 + mock_get.side_effect = [ + requests.exceptions.ConnectTimeout, + requests.exceptions.ConnectionError] + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises( + requests.exceptions.ConnectionError, + molds.get_configuration, + task, + 'https://example.com/file2') + mock_get.assert_called_with( + 'https://example.com/file2', + headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) + self.assertEqual(mock_get.call_count, 2) diff --git a/ironic/tests/unit/drivers/test_utils.py b/ironic/tests/unit/drivers/test_utils.py index 206a4340ad..f2e79e8271 100644 --- a/ironic/tests/unit/drivers/test_utils.py +++ b/ironic/tests/unit/drivers/test_utils.py @@ -19,7 +19,6 @@ from unittest import mock from oslo_config import cfg from oslo_utils import timeutils -import requests from ironic.common import exception from ironic.common import swift @@ -409,270 +408,3 @@ class MixinVendorInterfaceTestCase(db_base.DbTestCase): self.assertRaises(exception.InvalidParameterValue, self.vendor.validate, task, method='fake_method') - - -class ConfigurationMoldTestCase(db_base.DbTestCase): - - def setUp(self): - super(ConfigurationMoldTestCase, self).setUp() - self.node = obj_utils.create_test_node(self.context) - - @mock.patch.object(swift, 'get_swift_session', autospec=True) - @mock.patch.object(requests, 'put', autospec=True) - def test_save_configuration_mold_swift(self, mock_put, mock_swift): - mock_session = mock.Mock() - mock_session.get_token.return_value = 'token' - mock_swift.return_value = mock_session - driver_utils.CONF.mold_storage = 'swift' - url = 'https://example.com/file1' - data = {'key': 'value'} - - with task_manager.acquire(self.context, self.node.uuid) as task: - driver_utils.save_configuration_mold(task, url, data) - - mock_put.assert_called_once_with(url, '{\n "key": "value"\n}', - headers={'X-Auth-Token': 'token'}) - - @mock.patch.object(swift, 'get_swift_session', autospec=True) - @mock.patch.object(requests, 'put', autospec=True) - def test_save_configuration_mold_swift_noauth(self, mock_put, mock_swift): - mock_session = mock.Mock() - mock_session.get_token.return_value = None - mock_swift.return_value = mock_session - driver_utils.CONF.mold_storage = 'swift' - url = 'https://example.com/file1' - data = {'key': 'value'} - - with task_manager.acquire(self.context, self.node.uuid) as task: - self.assertRaises( - exception.IronicException, - driver_utils.save_configuration_mold, - task, url, data) - - @mock.patch.object(requests, 'put', autospec=True) - def test_save_configuration_mold_http(self, mock_put): - driver_utils.CONF.mold_storage = 'http' - driver_utils.CONF.mold_user = 'user' - driver_utils.CONF.mold_password = 'password' - url = 'https://example.com/file1' - data = {'key': 'value'} - - with task_manager.acquire(self.context, self.node.uuid) as task: - driver_utils.save_configuration_mold(task, url, data) - - mock_put.assert_called_once_with( - url, '{\n "key": "value"\n}', - headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) - - @mock.patch.object(requests, 'put', autospec=True) - def test_save_configuration_mold_http_noauth(self, mock_put): - driver_utils.CONF.mold_storage = 'http' - driver_utils.CONF.mold_user = None - driver_utils.CONF.mold_password = None - url = 'https://example.com/file1' - data = {'key': 'value'} - - with task_manager.acquire(self.context, self.node.uuid) as task: - driver_utils.save_configuration_mold(task, url, data) - - mock_put.assert_called_once_with( - url, '{\n "key": "value"\n}', - headers=None) - - @mock.patch.object(requests, 'put', autospec=True) - def test_save_configuration_mold_http_error(self, mock_put): - driver_utils.CONF.mold_storage = 'http' - driver_utils.CONF.mold_user = 'user' - driver_utils.CONF.mold_password = 'password' - response = mock.MagicMock() - response.status_code = 404 - response.raise_for_status.side_effect = requests.exceptions.HTTPError - mock_put.return_value = response - - with task_manager.acquire(self.context, self.node.uuid) as task: - self.assertRaises( - requests.exceptions.HTTPError, - driver_utils.save_configuration_mold, - task, - 'https://example.com/file2', - {'key': 'value'}) - mock_put.assert_called_once_with( - 'https://example.com/file2', '{\n "key": "value"\n}', - headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) - - @mock.patch.object(requests, 'put', autospec=True) - def test_save_configuration_mold_connection_error(self, mock_put): - driver_utils.CONF.mold_storage = 'http' - driver_utils.CONF.mold_user = 'user' - driver_utils.CONF.mold_password = 'password' - driver_utils.CONF.mold_retry_interval = 0 - driver_utils.CONF.mold_retry_attempts = 3 - response = mock.MagicMock() - mock_put.side_effect = [ - requests.exceptions.ConnectTimeout, - requests.exceptions.ConnectionError, - response] - - with task_manager.acquire(self.context, self.node.uuid) as task: - driver_utils.save_configuration_mold( - task, 'https://example.com/file2', {'key': 'value'}) - mock_put.assert_called_with( - 'https://example.com/file2', '{\n "key": "value"\n}', - headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) - self.assertEqual(mock_put.call_count, 3) - - @mock.patch.object(requests, 'put', autospec=True) - def test_save_configuration_mold_connection_error_exceeded(self, mock_put): - driver_utils.CONF.mold_storage = 'http' - driver_utils.CONF.mold_user = 'user' - driver_utils.CONF.mold_password = 'password' - driver_utils.CONF.mold_retry_interval = 0 - driver_utils.CONF.mold_retry_attempts = 2 - mock_put.side_effect = [ - requests.exceptions.ConnectTimeout, - requests.exceptions.ConnectionError] - - with task_manager.acquire(self.context, self.node.uuid) as task: - self.assertRaises( - requests.exceptions.ConnectionError, - driver_utils.save_configuration_mold, - task, - 'https://example.com/file2', - {'key': 'value'}) - mock_put.assert_called_with( - 'https://example.com/file2', '{\n "key": "value"\n}', - headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) - self.assertEqual(mock_put.call_count, 2) - - @mock.patch.object(swift, 'get_swift_session', autospec=True) - @mock.patch.object(requests, 'get', autospec=True) - def test_get_configuration_mold_swift(self, mock_get, mock_swift): - mock_session = mock.Mock() - mock_session.get_token.return_value = 'token' - mock_swift.return_value = mock_session - driver_utils.CONF.mold_storage = 'swift' - response = mock.MagicMock() - response.status_code = 200 - response.json.return_value = {'key': 'value'} - mock_get.return_value = response - url = 'https://example.com/file1' - - with task_manager.acquire(self.context, self.node.uuid) as task: - result = driver_utils.get_configuration_mold(task, url) - - mock_get.assert_called_once_with( - url, headers={'X-Auth-Token': 'token'}) - self.assertJsonEqual({'key': 'value'}, result) - - @mock.patch.object(swift, 'get_swift_session', autospec=True) - @mock.patch.object(requests, 'get', autospec=True) - def test_get_configuration_mold_swift_noauth(self, mock_get, mock_swift): - mock_session = mock.Mock() - mock_session.get_token.return_value = None - mock_swift.return_value = mock_session - driver_utils.CONF.mold_storage = 'swift' - url = 'https://example.com/file1' - - with task_manager.acquire(self.context, self.node.uuid) as task: - self.assertRaises( - exception.IronicException, - driver_utils.get_configuration_mold, - task, url) - - @mock.patch.object(requests, 'get', autospec=True) - def test_get_configuration_mold_http(self, mock_get): - driver_utils.CONF.mold_storage = 'http' - driver_utils.CONF.mold_user = 'user' - driver_utils.CONF.mold_password = 'password' - response = mock.MagicMock() - response.status_code = 200 - response.json.return_value = {'key': 'value'} - mock_get.return_value = response - url = 'https://example.com/file2' - - with task_manager.acquire(self.context, self.node.uuid) as task: - result = driver_utils.get_configuration_mold(task, url) - - mock_get.assert_called_once_with( - url, headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) - self.assertJsonEqual({"key": "value"}, result) - - @mock.patch.object(requests, 'get', autospec=True) - def test_get_configuration_mold_http_noauth(self, mock_get): - driver_utils.CONF.mold_storage = 'http' - driver_utils.CONF.mold_user = None - driver_utils.CONF.mold_password = None - response = mock.MagicMock() - response.status_code = 200 - response.json.return_value = {'key': 'value'} - mock_get.return_value = response - url = 'https://example.com/file2' - - with task_manager.acquire(self.context, self.node.uuid) as task: - result = driver_utils.get_configuration_mold(task, url) - - mock_get.assert_called_once_with(url, headers=None) - self.assertJsonEqual({"key": "value"}, result) - - @mock.patch.object(requests, 'get', autospec=True) - def test_get_configuration_mold_http_error(self, mock_get): - driver_utils.CONF.mold_storage = 'http' - driver_utils.CONF.mold_user = 'user' - driver_utils.CONF.mold_password = 'password' - response = mock.MagicMock() - response.status_code = 404 - response.raise_for_status.side_effect = requests.exceptions.HTTPError - mock_get.return_value = response - - with task_manager.acquire(self.context, self.node.uuid) as task: - self.assertRaises( - requests.exceptions.HTTPError, - driver_utils.get_configuration_mold, - task, - 'https://example.com/file2') - mock_get.assert_called_once_with( - 'https://example.com/file2', - headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) - - @mock.patch.object(requests, 'get', autospec=True) - def test_get_configuration_mold_connection_error(self, mock_get): - driver_utils.CONF.mold_storage = 'http' - driver_utils.CONF.mold_user = 'user' - driver_utils.CONF.mold_password = 'password' - driver_utils.CONF.mold_retry_interval = 0 - driver_utils.CONF.mold_retry_attempts = 3 - response = mock.MagicMock() - mock_get.side_effect = [ - requests.exceptions.ConnectTimeout, - requests.exceptions.ConnectionError, - response] - - with task_manager.acquire(self.context, self.node.uuid) as task: - driver_utils.get_configuration_mold( - task, 'https://example.com/file2') - mock_get.assert_called_with( - 'https://example.com/file2', - headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) - self.assertEqual(mock_get.call_count, 3) - - @mock.patch.object(requests, 'get', autospec=True) - def test_get_configuration_mold_connection_error_exceeded(self, mock_get): - driver_utils.CONF.mold_storage = 'http' - driver_utils.CONF.mold_user = 'user' - driver_utils.CONF.mold_password = 'password' - driver_utils.CONF.mold_retry_interval = 0 - driver_utils.CONF.mold_retry_attempts = 2 - mock_get.side_effect = [ - requests.exceptions.ConnectTimeout, - requests.exceptions.ConnectionError] - - with task_manager.acquire(self.context, self.node.uuid) as task: - self.assertRaises( - requests.exceptions.ConnectionError, - driver_utils.get_configuration_mold, - task, - 'https://example.com/file2') - mock_get.assert_called_with( - 'https://example.com/file2', - headers={'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='}) - self.assertEqual(mock_get.call_count, 2)