From 58b34b0b3002cd5bc355bfb0e6f5406004a4da41 Mon Sep 17 00:00:00 2001 From: Anup Navare Date: Mon, 5 Dec 2016 22:50:55 +0000 Subject: [PATCH] Config drive support for ceph radosgw Currently config drive can be stored in swift with keystone authentication. This change allows ironic to store the config drive in ceph radosgw and use radosgw authentication mechanism that is not currently supported. It uses swift API compatibility for ceph radosgw. New options: [deploy]/configdrive_use_object_store [deploy]/object_store_endpoint_type Deprecations: [conductor]/configdrive_use_swift Replaced by: [deploy]/configdrive_use_object_store [glance]/temp_url_endpoint_type Replaced by: [deploy]/object_store_endpoint_type Change-Id: I9204c718505376cfb73632b0d0f31cea00d5e4d8 Closes-Bug: #1642719 --- doc/source/deploy/radosgw.rst | 5 +- etc/ironic/ironic.conf.sample | 23 ++++---- install-guide/source/configdrive.rst | 53 ++++++++++++++++++- .../common/glance_service/v2/image_service.py | 4 +- ironic/common/swift.py | 20 +++++-- ironic/conductor/base_manager.py | 14 +++++ ironic/conductor/manager.py | 13 +++-- ironic/conf/conductor.py | 6 +-- ironic/conf/deploy.py | 14 +++++ ironic/conf/glance.py | 6 --- .../tests/unit/common/test_glance_service.py | 8 +-- ironic/tests/unit/common/test_swift.py | 27 ++++++++++ .../tests/unit/conductor/test_base_manager.py | 22 ++++++++ ironic/tests/unit/conductor/test_manager.py | 6 ++- ...t-using-ceph-radosgw-8c6f7b8bede2077c.yaml | 23 ++++++++ 15 files changed, 206 insertions(+), 38 deletions(-) create mode 100644 releasenotes/notes/configdrive-support-using-ceph-radosgw-8c6f7b8bede2077c.yaml diff --git a/doc/source/deploy/radosgw.rst b/doc/source/deploy/radosgw.rst index 1c517d1b53..a18788abfc 100644 --- a/doc/source/deploy/radosgw.rst +++ b/doc/source/deploy/radosgw.rst @@ -64,6 +64,9 @@ Configure Ironic and Glance with RADOS Gateway swift_api_version = v1 swift_endpoint_url = http://RADOS_IP:PORT swift_temp_url_key = TEMP_URL_KEY - temp_url_endpoint_type=radosgw + + [deploy] + + object_store_endpoint_type = radosgw #. Restart Ironic conductor service(s). diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index 001ad5e4d5..bde870b055 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -996,11 +996,8 @@ # the check entirely. (integer value) #sync_local_state_interval = 180 -# Whether to upload the config drive to Swift. (boolean value) -#configdrive_use_swift = false - # Name of the Swift container to store config drive data. Used -# when configdrive_use_swift is True. (string value) +# when configdrive_use_object_store is True. (string value) #configdrive_swift_container = ironic_configdrive_container # Timeout (seconds) for waiting for node inspection. 0 - @@ -1312,6 +1309,18 @@ # Allowed values: netboot, local #default_boot_option = +# Whether to upload the config drive to object store. Set this +# option to True to store config drive in swift or radosgw. +# (boolean value) +# Deprecated group/name - [conductor]/configdrive_use_swift +#configdrive_use_object_store = false + +# Type of object store endpoint type to be used as a backend +# (string value) +# Allowed values: swift, radosgw +# Deprecated group/name - [glance]/temp_url_endpoint_type +#object_store_endpoint_type = swift + [dhcp] @@ -1561,12 +1570,6 @@ # downloads. Required for temporary URLs. (string value) #swift_temp_url_key = -# Type of endpoint to use for temporary URLs. If the Glance -# backend is Swift, use "swift"; if it is CEPH with RADOS -# gateway, use "radosgw". (string value) -# Allowed values: swift, radosgw -#temp_url_endpoint_type = swift - # Tenant ID (string value) #tenant_id = diff --git a/install-guide/source/configdrive.rst b/install-guide/source/configdrive.rst index 42b4ff7046..b900a9b822 100644 --- a/install-guide/source/configdrive.rst +++ b/install-guide/source/configdrive.rst @@ -58,6 +58,54 @@ for example:: ironic node-set-provision-state --config-drive /dir/configdrive_files $node_identifier active +Configuration drive storage in an object store +---------------------------------------------- + +Under normal circumstances, the configuration drive can be stored in the +Bare Metal service when the size is less than 64KB. Optionally, if the size +is larger than 64KB there is support to store it in swift or radosgw backed +object store. Both swift and radosgw use swift-style APIs. + +The following option in ``/etc/ironic/ironic.conf`` enables swift as an object +store backend to store config drive. This uses the Identity service to +establish a session between the Bare Metal service and the +Object Storage service. :: + + [deploy] + ... + + configdrive_use_object_store = True + +Use the following options in ``/etc/ironic/ironic.conf`` to enable radosgw. +Credentials in the swift section are needed because radosgw will not use the +Identity service and relies on radosgw's username and password authentication +instead. :: + + [deploy] + ... + + configdrive_use_object_store = True + object_store_endpoint_type = radosgw + + [swift] + ... + + username = USERNAME + password = PASSWORD + auth_url = http://RADOSGW_IP:8000/auth/v1 + +Make sure that if an agent_* driver is being used, edit +``/etc/glance/glance-api.conf`` to store the instance images in respective +object store (radosgw or swift) as well:: + + [glance_store] + ... + + swift_store_user = USERNAME + swift_store_key = PASSWORD + swift_store_auth_address = http://RADOSGW_OR_SWIFT_IP:PORT/auth/v1 + + Accessing the configuration drive data -------------------------------------- @@ -81,10 +129,13 @@ the configuration drive and mount it, for example:: mount $CONFIG_DEV /mnt/config -.. [*] A config drive could also be a data block with a VFAT filesystem +.. [*] A configuration drive could also be a data block with a VFAT filesystem on it instead of ISO 9660. But it's unlikely that it would be needed since ISO 9660 is widely supported across operating systems. +For more information see `Store metadata on a configuration drive +`_. + Cloud-init integration ---------------------- diff --git a/ironic/common/glance_service/v2/image_service.py b/ironic/common/glance_service/v2/image_service.py index 7f9b13e897..b4c54bc5d5 100644 --- a/ironic/common/glance_service/v2/image_service.py +++ b/ironic/common/glance_service/v2/image_service.py @@ -147,7 +147,7 @@ class GlanceImageService(base_image_service.BaseImageService, } endpoint_url = CONF.glance.swift_endpoint_url - if CONF.glance.temp_url_endpoint_type == 'radosgw': + if CONF.deploy.object_store_endpoint_type == 'radosgw': chunks = urlparse.urlsplit(CONF.glance.swift_endpoint_url) if not chunks.path: endpoint_url = urlparse.urljoin( @@ -184,7 +184,7 @@ class GlanceImageService(base_image_service.BaseImageService, 'Swift temporary URLs require a Swift endpoint URL. ' 'You must provide "swift_endpoint_url" as a config option.')) if (not CONF.glance.swift_account and - CONF.glance.temp_url_endpoint_type == 'swift'): + CONF.deploy.object_store_endpoint_type == 'swift'): raise exc.MissingParameterValue(_( 'Swift temporary URLs require a Swift account string. ' 'You must provide "swift_account" as a config option.')) diff --git a/ironic/common/swift.py b/ironic/common/swift.py index a11736003e..1203ed0732 100644 --- a/ironic/common/swift.py +++ b/ironic/common/swift.py @@ -23,7 +23,7 @@ from swiftclient import utils as swift_utils from ironic.common import exception from ironic.common.i18n import _ from ironic.common import keystone - +from ironic.conf import CONF _SWIFT_SESSION = None @@ -39,8 +39,22 @@ class SwiftAPI(object): """API for communicating with Swift.""" def __init__(self): - session = _get_swift_session() - self.connection = swift_client.Connection(session=session) + """Initialize the connection with swift or radosgw + + :raises: ConfigInvalid if required keystone authorization credentials + with swift are missing. + """ + params = {} + if CONF.deploy.object_store_endpoint_type == 'radosgw': + params = {'authurl': CONF.swift.auth_url, + 'user': CONF.swift.username, + 'key': CONF.swift.password} + else: + # NOTE(aNuposic): Session will be initiated only when connection + # with swift is initialized. Since v3.2.0 swiftclient supports + # instantiating the API client from keystoneauth session. + params = {'session': _get_swift_session()} + self.connection = swift_client.Connection(**params) def create_object(self, container, obj, filename, object_headers=None): diff --git a/ironic/conductor/base_manager.py b/ironic/conductor/base_manager.py index 90a59d55a2..618ffbc70e 100644 --- a/ironic/conductor/base_manager.py +++ b/ironic/conductor/base_manager.py @@ -85,6 +85,8 @@ class BaseConductorManager(object): :raises: DriverLoadError if an enabled driver cannot be loaded. :raises: DriverNameConflict if a classic driver and a dynamic driver are both enabled and have the same name. + :raises: ConfigInvalid if required config options for connection with + radosgw are missing while storing config drive. """ if self._started: raise RuntimeError(_('Attempt to start an already running ' @@ -172,6 +174,18 @@ class BaseConductorManager(object): self._periodic_task_callables, executor_factory=periodics.ExistingExecutor(self._executor)) + # Check for required config options if object_store_endpoint_type is + # radosgw + if (CONF.deploy.configdrive_use_object_store and + CONF.deploy.object_store_endpoint_type == "radosgw"): + if (None in (CONF.swift.auth_url, CONF.swift.username, + CONF.swift.password)): + msg = _("Parameters missing to make a connection with " + "radosgw. Ensure that [swift]/auth_url, " + "[swift]/username, and [swift]/password are all " + "configured.") + raise exception.ConfigInvalid(msg) + # clear all target_power_state with locks by this conductor self.dbapi.clear_node_target_power_state(self.host) # clear all locks held by this conductor before registering diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index c22513cfb2..990b03b7e6 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -2698,17 +2698,20 @@ def _get_configdrive_obj_name(node): def _store_configdrive(node, configdrive): """Handle the storage of the config drive. - If configured, the config drive data are uploaded to Swift. The Node's - instance_info is updated to include either the temporary Swift URL - from the upload, or if no upload, the actual config drive data. + If configured, the config drive data are uploaded to swift or radosgw. + The Node's instance_info is updated to include either the temporary + Swift URL from the upload, or if no upload, the actual config drive data. :param node: an Ironic node object. :param configdrive: A gzipped and base64 encoded configdrive. :raises: SwiftOperationError if an error occur when uploading the - config drive to Swift. + config drive to swift or radosgw. + :raises: ConfigInvalid if required keystone authorization credentials + with swift are missing. + """ - if CONF.conductor.configdrive_use_swift: + if CONF.deploy.configdrive_use_object_store: # NOTE(lucasagomes): No reason to use a different timeout than # the one used for deploying the node timeout = CONF.conductor.deploy_callback_timeout diff --git a/ironic/conf/conductor.py b/ironic/conf/conductor.py index 01b6c37a1e..a9846b088d 100644 --- a/ironic/conf/conductor.py +++ b/ironic/conf/conductor.py @@ -107,13 +107,11 @@ opts = [ 'conductor will check for nodes that it should ' '"take over". Set it to a negative value to disable ' 'the check entirely.')), - cfg.BoolOpt('configdrive_use_swift', - default=False, - help=_('Whether to upload the config drive to Swift.')), cfg.StrOpt('configdrive_swift_container', default='ironic_configdrive_container', help=_('Name of the Swift container to store config drive ' - 'data. Used when configdrive_use_swift is True.')), + 'data. Used when configdrive_use_object_store is ' + 'True.')), cfg.IntOpt('inspect_timeout', default=1800, help=_('Timeout (seconds) for waiting for node inspection. ' diff --git a/ironic/conf/deploy.py b/ironic/conf/deploy.py index 3e0ce1ea3a..e11b075d46 100644 --- a/ironic/conf/deploy.py +++ b/ironic/conf/deploy.py @@ -72,6 +72,20 @@ opts = [ 'default is "netboot", but it will be changed to ' '"local" in the future. It is recommended to set ' 'an explicit value for this option.')), + cfg.BoolOpt('configdrive_use_object_store', + default=False, + deprecated_group='conductor', + deprecated_name='configdrive_use_swift', + help=_('Whether to upload the config drive to object store. ' + 'Set this option to True to store config drive ' + 'in swift or radosgw.')), + cfg.StrOpt('object_store_endpoint_type', + default='swift', + deprecated_group='glance', + deprecated_name='temp_url_endpoint_type', + choices=['swift', 'radosgw'], + help=_('Type of object store endpoint type to be ' + 'used as a backend')), ] diff --git a/ironic/conf/glance.py b/ironic/conf/glance.py index 99f49d07f5..b29500e321 100644 --- a/ironic/conf/glance.py +++ b/ironic/conf/glance.py @@ -103,12 +103,6 @@ opts = [ 'value between 1 and 32, a single-tenant store will use ' 'multiple containers to store images, and this value ' 'will determine how many containers are created.')), - cfg.StrOpt('temp_url_endpoint_type', - default='swift', - choices=['swift', 'radosgw'], - help=_('Type of endpoint to use for temporary URLs. If the ' - 'Glance backend is Swift, use "swift"; if it is CEPH ' - 'with RADOS gateway, use "radosgw".')), cfg.StrOpt('glance_host', default='$my_ip', help=_('Default glance hostname or IP address.')), diff --git a/ironic/tests/unit/common/test_glance_service.py b/ironic/tests/unit/common/test_glance_service.py index 951bfde2f7..e2768bc404 100644 --- a/ironic/tests/unit/common/test_glance_service.py +++ b/ironic/tests/unit/common/test_glance_service.py @@ -744,7 +744,7 @@ class TestGlanceSwiftTempURL(base.TestCase): @mock.patch('swiftclient.utils.generate_temp_url', autospec=True) def test_swift_temp_url_radosgw(self, tempurl_mock): - self.config(temp_url_endpoint_type='radosgw', group='glance') + self.config(object_store_endpoint_type='radosgw', group='deploy') path = ('/v1' '/glance' '/757274c4-2856-4bd2-bb20-9a4a231e187b') @@ -769,7 +769,7 @@ class TestGlanceSwiftTempURL(base.TestCase): def test_swift_temp_url_radosgw_endpoint_with_swift(self, tempurl_mock): self.config(swift_endpoint_url='https://swift.radosgw.com/swift', group='glance') - self.config(temp_url_endpoint_type='radosgw', group='glance') + self.config(object_store_endpoint_type='radosgw', group='deploy') path = ('/v1' '/glance' '/757274c4-2856-4bd2-bb20-9a4a231e187b') @@ -793,7 +793,7 @@ class TestGlanceSwiftTempURL(base.TestCase): def test_swift_temp_url_radosgw_endpoint_invalid(self, tempurl_mock): self.config(swift_endpoint_url='https://swift.radosgw.com/eggs/', group='glance') - self.config(temp_url_endpoint_type='radosgw', group='glance') + self.config(object_store_endpoint_type='radosgw', group='deploy') self.service._validate_temp_url_config = mock.Mock() self.assertRaises(exception.InvalidParameterValue, @@ -851,7 +851,7 @@ class TestGlanceSwiftTempURL(base.TestCase): def test__validate_temp_url_no_account_exception_radosgw(self): self.config(swift_account=None, group='glance') - self.config(temp_url_endpoint_type='radosgw', group='glance') + self.config(object_store_endpoint_type='radosgw', group='deploy') self.service._validate_temp_url_config() def test__validate_temp_url_endpoint_less_than_download_delay(self): diff --git a/ironic/tests/unit/common/test_swift.py b/ironic/tests/unit/common/test_swift.py index 305881a1a7..f9b07e4baa 100644 --- a/ironic/tests/unit/common/test_swift.py +++ b/ironic/tests/unit/common/test_swift.py @@ -13,6 +13,7 @@ # under the License. import mock +from oslo_config import cfg import six from six.moves import builtins as __builtin__ from six.moves import http_client @@ -24,6 +25,7 @@ from ironic.common import exception from ironic.common import swift from ironic.tests import base +CONF = cfg.CONF if six.PY3: import io @@ -39,10 +41,35 @@ class SwiftTestCase(base.TestCase): self.swift_exception = swift_exception.ClientException('', '') def test___init__(self, connection_mock, keystone_mock): + """Check if client is properly initialized with swift""" + swift.SwiftAPI() connection_mock.assert_called_once_with( session=keystone_mock.return_value) + def test___init___radosgw(self, connection_mock, swift_session_mock): + """Check if client is properly initialized with radosgw""" + + auth_url = 'http://1.2.3.4' + username = 'foo' + password = 'foo_password' + CONF.set_override('object_store_endpoint_type', 'radosgw', + group='deploy') + opts = [cfg.StrOpt('auth_url'), cfg.StrOpt('username'), + cfg.StrOpt('password')] + CONF.register_opts(opts, group='swift') + + CONF.set_override('auth_url', auth_url, group='swift') + CONF.set_override('username', username, group='swift') + CONF.set_override('password', password, group='swift') + + swift.SwiftAPI() + params = {'authurl': auth_url, + 'user': username, + 'key': password} + connection_mock.assert_called_once_with(**params) + swift_session_mock.assert_not_called() + @mock.patch.object(__builtin__, 'open', autospec=True) def test_create_object(self, open_mock, connection_mock, keystone_mock): swiftapi = swift.SwiftAPI() diff --git a/ironic/tests/unit/conductor/test_base_manager.py b/ironic/tests/unit/conductor/test_base_manager.py index c7f1fd94e6..7e29a395e6 100644 --- a/ironic/tests/unit/conductor/test_base_manager.py +++ b/ironic/tests/unit/conductor/test_base_manager.py @@ -250,6 +250,28 @@ class StartStopTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): self.service.del_host() self.assertTrue(wait_mock.called) + def test_start_fails_on_missing_config_for_configdrive(self): + """Check to fail conductor on missing config options""" + + missing_parameters_error = ("Parameters missing to make a " + "connection with radosgw") + CONF.set_override('configdrive_use_object_store', True, + group='deploy') + CONF.set_override('object_store_endpoint_type', 'radosgw', + group='deploy') + params = {'auth_url': 'http://1.2.3.4', + 'username': 'foo', 'password': 'foo_pass'} + CONF.register_opts((cfg.StrOpt(x) for x in params), + group='swift') + for key, value in params.items(): + test_params = params.copy() + test_params[key] = None + for test_key, test_value in test_params.items(): + CONF.set_override(key, test_value, group='swift') + with self.assertRaisesRegex(exception.ConfigInvalid, + missing_parameters_error): + self._start_service() + class CheckInterfacesTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index 82e0c163d3..3bd65b480f 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -1433,7 +1433,8 @@ class DoNodeDeployTearDownTestCase(mgr_utils.ServiceSetUpMixin, @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') def test__do_node_deploy_configdrive_swift_error(self, mock_deploy, mock_swift): - CONF.set_override('configdrive_use_swift', True, group='conductor') + CONF.set_override('configdrive_use_object_store', True, + group='deploy') self._start_service() # test when driver.deploy.deploy returns DEPLOYDONE mock_deploy.return_value = states.DEPLOYDONE @@ -5052,7 +5053,8 @@ class StoreConfigDriveTestCase(tests_base.TestCase): expected_instance_info = {'configdrive': 'http://1.2.3.4'} # set configs and mocks - CONF.set_override('configdrive_use_swift', True, group='conductor') + CONF.set_override('configdrive_use_object_store', True, + group='deploy') CONF.set_override('configdrive_swift_container', container_name, group='conductor') CONF.set_override('deploy_callback_timeout', timeout, diff --git a/releasenotes/notes/configdrive-support-using-ceph-radosgw-8c6f7b8bede2077c.yaml b/releasenotes/notes/configdrive-support-using-ceph-radosgw-8c6f7b8bede2077c.yaml new file mode 100644 index 0000000000..e47a9b1901 --- /dev/null +++ b/releasenotes/notes/configdrive-support-using-ceph-radosgw-8c6f7b8bede2077c.yaml @@ -0,0 +1,23 @@ +--- +features: + - Adds support for storing the configdrive in radosgw using + the swift API. + - | + Adds support to use the radosgw authentication mechanism that relies + on username and password instead of auth token. + The following options must be specified in ironic configuration file: + + * ``[swift]/auth_url`` + * ``[swift]/username`` + * ``[swift]/password`` + +deprecations: + - The ``[conductor]/configdrive_use_swift`` and + ``[glance]/temp_url_endpoint_type`` options are deprecated and will be + removed in the Queens release. + Use ``[deploy]/configdrive_use_object_store`` and + ``[deploy]/object_store_endpoint_type`` respectively instead. +upgrade: + - Adds a ``[deploy]/object_store_endpoint_type`` option to specify the + type of endpoint to use for instance images and configdrive storage. + Allowed values are 'swift' or 'radosgw'. The default is 'swift'.