Merge "Add an external storage interface"

This commit is contained in:
Zuul 2018-06-26 11:49:41 +00:00 committed by Gerrit Code Review
commit 5b199fa3dc
6 changed files with 209 additions and 1 deletions
doc/source/admin
ironic
drivers
generic.py
modules/storage
tests/unit/drivers/modules/storage
releasenotes/notes
setup.cfg

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

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

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

@ -151,6 +151,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