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
This commit is contained in:
Julia Kreger 2018-04-10 15:10:20 -07:00 committed by Jim Rollenhagen
parent bfbe14b873
commit 5795c57985
6 changed files with 209 additions and 1 deletions

View File

@ -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
-------------------

View File

@ -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):

View File

@ -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

View File

@ -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))

View File

@ -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.

View File

@ -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