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:
parent
bfbe14b873
commit
5795c57985
@ -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
|
||||
-------------------
|
||||
|
||||
|
@ -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):
|
||||
|
67
ironic/drivers/modules/storage/external.py
Normal file
67
ironic/drivers/modules/storage/external.py
Normal 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
|
68
ironic/tests/unit/drivers/modules/storage/test_external.py
Normal file
68
ironic/tests/unit/drivers/modules/storage/test_external.py
Normal 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))
|
@ -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.
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user