summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/source/admin/boot-from-volume.rst57
-rw-r--r--ironic/drivers/generic.py4
-rw-r--r--ironic/drivers/modules/storage/external.py67
-rw-r--r--ironic/tests/unit/drivers/modules/storage/test_external.py68
-rw-r--r--releasenotes/notes/adds-external-storage-interface-9b7c0a0a2afd3176.yaml13
-rw-r--r--setup.cfg1
6 files changed, 209 insertions, 1 deletions
diff --git a/doc/source/admin/boot-from-volume.rst b/doc/source/admin/boot-from-volume.rst
index da875cf25..3c13ea1f5 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 5292ec079..6f3d280e5 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 000000000..ad7d7e6da
--- /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 000000000..b337a8dc5
--- /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 000000000..2fad940a8
--- /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 <https://docs.openstack.org/ironic/latest/admin/boot-from-volume.html#use-without-cinder>`_
+ has been updated to reflect how to use this interface.
diff --git a/setup.cfg b/setup.cfg
index 7b4eb9aec..ce37677ae 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