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,
|
node. As the ``boot-index`` is per-node in sequential order,
|
||||||
only one boot volume is permitted for each node.
|
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
|
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 noop
|
||||||
from ironic.drivers.modules import pxe
|
from ironic.drivers.modules import pxe
|
||||||
from ironic.drivers.modules.storage import cinder
|
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
|
from ironic.drivers.modules.storage import noop as noop_storage
|
||||||
|
|
||||||
|
|
||||||
@ -78,7 +79,8 @@ class GenericHardware(hardware_type.AbstractHardwareType):
|
|||||||
@property
|
@property
|
||||||
def supported_storage_interfaces(self):
|
def supported_storage_interfaces(self):
|
||||||
"""List of supported storage interfaces."""
|
"""List of supported storage interfaces."""
|
||||||
return [noop_storage.NoopStorage, cinder.CinderStorage]
|
return [noop_storage.NoopStorage, cinder.CinderStorage,
|
||||||
|
external_storage.ExternalStorage]
|
||||||
|
|
||||||
|
|
||||||
class ManualManagementHardware(GenericHardware):
|
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
|
fake = ironic.drivers.modules.fake:FakeStorage
|
||||||
noop = ironic.drivers.modules.storage.noop:NoopStorage
|
noop = ironic.drivers.modules.storage.noop:NoopStorage
|
||||||
cinder = ironic.drivers.modules.storage.cinder:CinderStorage
|
cinder = ironic.drivers.modules.storage.cinder:CinderStorage
|
||||||
|
external = ironic.drivers.modules.storage.external:ExternalStorage
|
||||||
|
|
||||||
ironic.hardware.interfaces.vendor =
|
ironic.hardware.interfaces.vendor =
|
||||||
fake = ironic.drivers.modules.fake:FakeVendorB
|
fake = ironic.drivers.modules.fake:FakeVendorB
|
||||||
|
Loading…
x
Reference in New Issue
Block a user