From 5795c57985e65cd96c3a71e1329e578a1e947449 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 10 Apr 2018 15:10:20 -0700 Subject: [PATCH] Add an external storage interface This would primarily be very useful for users of an external SAN image based management solution[0] where the interaction with the storage system has been abstracted from the user but iSCSI targets are still used. [0]: https://massopen.cloud/blog/bare-metal-imaging/ Change-Id: I2d45b8a7023d053aac24e106bb027b9d0408cf3a Story: #1735478 Task: #12562 --- doc/source/admin/boot-from-volume.rst | 57 ++++++++++++++++ ironic/drivers/generic.py | 4 +- ironic/drivers/modules/storage/external.py | 67 ++++++++++++++++++ .../drivers/modules/storage/test_external.py | 68 +++++++++++++++++++ ...al-storage-interface-9b7c0a0a2afd3176.yaml | 13 ++++ setup.cfg | 1 + 6 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 ironic/drivers/modules/storage/external.py create mode 100644 ironic/tests/unit/drivers/modules/storage/test_external.py create mode 100644 releasenotes/notes/adds-external-storage-interface-9b7c0a0a2afd3176.yaml diff --git a/doc/source/admin/boot-from-volume.rst b/doc/source/admin/boot-from-volume.rst index da875cf255..3c13ea1f57 100644 --- a/doc/source/admin/boot-from-volume.rst +++ b/doc/source/admin/boot-from-volume.rst @@ -93,6 +93,63 @@ A target record can be created using a command similar to the example below:: node. As the ``boot-index`` is per-node in sequential order, only one boot volume is permitted for each node. +Use Without Cinder +------------------ + +In the Rocky release, an ``external`` storage interface is available that +can be utilized without a Block Storage Service installation. + +Under normal circumstances the ``cinder`` storage interface +interacts with the Block Storage Service to orchestrate and manage +attachment and detachment of volumes from the underlying block service +system. + +The ``external`` storage interface contains the logic to allow the Bare +Metal service to determine if the Bare Metal node has been requested with +a remote storage volume for booting. This is in contrast to the default +``noop`` storage interface which does not contain logic to determine if +the node should or could boot from a remote volume. + +It must be noted that minimal configuration or value validation occurs +with the ``external`` storage interface. The ``cinder`` storage interface +contains more extensive validation, that is likely un-necessary in a +``external`` scenario. + +Setting the external storage interface:: + + openstack baremetal node set --storage-interface external $NODE_UUID + +Setting a volume:: + + openstack baremetal volume target create --node $NODE_UUID \ + --type iscsi --boot-index 0 --volume-id $VOLUME_UUID \ + --property target_iqn="iqn.2010-10.com.example:vol-X" \ + --property target_lun="0" \ + --property target_portal="192.168.0.123:3260" \ + --property auth_method="CHAP" \ + --property auth_username="ABC" \ + --property auth_password="XYZ" \ + +Ensure that no image_source is defined:: + + openstack baremetal node unset \ + --instance-info image_source $NODE_UUID + +Deploy the node:: + + openstack baremetal node deploy $NODE_UUID + +Upon deploy, the boot interface for the baremetal node will attempt +to either create iPXE configuration OR set boot parameters out-of-band via +the management controller. Such action is boot interface specific and may not +support all forms of volume target configuration. As of the Rocky release, +the bare metal service does not support writing an Operating System image +to a remote boot from volume target, so that also must be ensured by +the user in advance. + +Records of volume targets are removed upon the node being undeployed, +and as such are not presistent across deployments. + Cinder Multi-attach ------------------- diff --git a/ironic/drivers/generic.py b/ironic/drivers/generic.py index 5292ec0794..6f3d280e57 100644 --- a/ironic/drivers/generic.py +++ b/ironic/drivers/generic.py @@ -28,6 +28,7 @@ from ironic.drivers.modules.network import noop as noop_net from ironic.drivers.modules import noop from ironic.drivers.modules import pxe from ironic.drivers.modules.storage import cinder +from ironic.drivers.modules.storage import external as external_storage from ironic.drivers.modules.storage import noop as noop_storage @@ -78,7 +79,8 @@ class GenericHardware(hardware_type.AbstractHardwareType): @property def supported_storage_interfaces(self): """List of supported storage interfaces.""" - return [noop_storage.NoopStorage, cinder.CinderStorage] + return [noop_storage.NoopStorage, cinder.CinderStorage, + external_storage.ExternalStorage] class ManualManagementHardware(GenericHardware): diff --git a/ironic/drivers/modules/storage/external.py b/ironic/drivers/modules/storage/external.py new file mode 100644 index 0000000000..ad7d7e6daa --- /dev/null +++ b/ironic/drivers/modules/storage/external.py @@ -0,0 +1,67 @@ +# 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 oslo_log import log + +from ironic.common import exception +from ironic.drivers import base + +CONF = cfg.CONF + +LOG = log.getLogger(__name__) + + +class ExternalStorage(base.StorageInterface): + """Externally driven Storage Interface.""" + + def validate(self, task): + def _fail_validation(task, reason, + exception=exception.InvalidParameterValue): + msg = (_("Failed to validate external storage interface for node " + "%(node)s. %(reason)s") % + {'node': task.node.uuid, 'reason': reason}) + LOG.error(msg) + raise exception(msg) + + if (not self.should_write_image(task) + and not CONF.pxe.ipxe_enabled): + msg = _("The [pxe]/ipxe_enabled option must " + "be set to True to support network " + "booting to an iSCSI volume.") + _fail_validation(task, msg) + + def get_properties(self): + return {} + + def attach_volumes(self, task): + pass + + def detach_volumes(self, task): + pass + + def should_write_image(self, task): + """Determines if deploy should perform the image write-out. + + This enables the user to define a volume and Ironic understand + that the image may already exist and we may be booting to that volume. + + :param task: The task object. + :returns: True if the deployment write-out process should be + executed. + """ + instance_info = task.node.instance_info + if 'image_source' not in instance_info: + for volume in task.volume_targets: + if volume['boot_index'] == 0: + return False + return True diff --git a/ironic/tests/unit/drivers/modules/storage/test_external.py b/ironic/tests/unit/drivers/modules/storage/test_external.py new file mode 100644 index 0000000000..b337a8dc57 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/storage/test_external.py @@ -0,0 +1,68 @@ +# Copyright 2016 Hewlett Packard Enterprise Development Company LP. +# Copyright 2016 IBM Corp +# 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 mock + +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.storage import external +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.objects import utils as object_utils + + +class ExternalInterfaceTestCase(db_base.DbTestCase): + + def setUp(self): + super(ExternalInterfaceTestCase, self).setUp() + self.config(ipxe_enabled=True, + group='pxe') + self.config(enabled_storage_interfaces=['noop', 'external']) + self.interface = external.ExternalStorage() + + @mock.patch.object(external, 'LOG', autospec=True) + def test_validate_fails_with_ipxe_not_enabled(self, mock_log): + """Ensure a validation failure is raised when iPXE not enabled.""" + self.config(ipxe_enabled=False, group='pxe') + self.node = object_utils.create_test_node( + self.context, storage_interface='external') + object_utils.create_test_volume_connector( + self.context, node_id=self.node.id, type='iqn', + connector_id='foo.address') + object_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=0, volume_id='2345') + with task_manager.acquire(self.context, self.node.id) as task: + self.assertRaises(exception.InvalidParameterValue, + self.interface.validate, + task) + self.assertTrue(mock_log.error.called) + + # Prevent /httpboot validation on creating the node + @mock.patch('ironic.drivers.modules.pxe.PXEBoot.__init__', + lambda self: None) + def test_should_write_image(self): + self.node = object_utils.create_test_node( + self.context, storage_interface='external') + object_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=0, volume_id='1234') + + with task_manager.acquire(self.context, self.node.id) as task: + self.assertFalse(self.interface.should_write_image(task)) + + self.node.instance_info = {'image_source': 'fake-value'} + self.node.save() + + with task_manager.acquire(self.context, self.node.id) as task: + self.assertTrue(self.interface.should_write_image(task)) diff --git a/releasenotes/notes/adds-external-storage-interface-9b7c0a0a2afd3176.yaml b/releasenotes/notes/adds-external-storage-interface-9b7c0a0a2afd3176.yaml new file mode 100644 index 0000000000..2fad940a8a --- /dev/null +++ b/releasenotes/notes/adds-external-storage-interface-9b7c0a0a2afd3176.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Adds ``external`` storage interface which is short for + "externally managed". This adds logic to allow the Bare + Metal service to identify when a BFV scenario is being + requested based upon the configuration set for + ``volume targets``. + + The user must create the entry, and no syncronizaiton + with a Block Storage service will occur. + `Documentation `_ + has been updated to reflect how to use this interface. diff --git a/setup.cfg b/setup.cfg index 7b4eb9aec6..ce37677ae6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -163,6 +163,7 @@ ironic.hardware.interfaces.storage = fake = ironic.drivers.modules.fake:FakeStorage noop = ironic.drivers.modules.storage.noop:NoopStorage cinder = ironic.drivers.modules.storage.cinder:CinderStorage + external = ironic.drivers.modules.storage.external:ExternalStorage ironic.hardware.interfaces.vendor = fake = ironic.drivers.modules.fake:FakeVendorB