Merge "Allow 'openstack baremetal configure boot' to guess the root device"

This commit is contained in:
Jenkins 2016-04-06 08:02:50 +00:00 committed by Gerrit Code Review
commit 71386ff20d
4 changed files with 343 additions and 1 deletions

View File

@ -64,3 +64,7 @@ class ProfileMatchingError(Exception):
class PasswordFileNotFound(Exception):
"""Password file for the Heat stack not found in the current working dir"""
class RootDeviceDetectionError(Exception):
"""Failed to detect the root device"""

View File

@ -13,6 +13,7 @@
# under the License.
#
import ironic_inspector_client
import mock
from openstackclient.tests import utils
@ -68,8 +69,9 @@ class FakeBaremetalNodeClient(object):
class FakeInspectorClient(object):
def __init__(self, states=None):
def __init__(self, states=None, data=None):
self.states = states or {}
self.data = data or {}
self.on_introspection = []
def introspect(self, uuid):
@ -78,6 +80,12 @@ class FakeInspectorClient(object):
def get_status(self, uuid):
return self.states[uuid]
def get_data(self, uuid):
try:
return self.data[uuid]
except KeyError:
raise ironic_inspector_client.ClientError(mock.Mock())
class TestBaremetal(utils.TestCommand):

View File

@ -21,6 +21,7 @@ import os
import yaml
import fixtures
from oslo_utils import units
from tripleoclient import exceptions
from tripleoclient.tests.v1.baremetal import fakes
@ -1170,6 +1171,219 @@ class TestConfigureBaremetalBoot(fakes.TestBaremetal):
])
@mock.patch('openstackclient.common.utils.find_resource', autospec=True)
class TestConfigureBaremetalBootRootDeviceDetection(fakes.TestBaremetal):
def setUp(self):
super(TestConfigureBaremetalBootRootDeviceDetection, self).setUp()
# Get the command object to test
self.cmd = baremetal.ConfigureBaremetalBoot(self.app, None)
self.disks = [
{'name': '/dev/sda', 'size': 11 * units.Gi},
{'name': '/dev/sdb', 'size': 2 * units.Gi},
{'name': '/dev/sdc', 'size': 5 * units.Gi},
{'name': '/dev/sdd', 'size': 21 * units.Gi},
{'name': '/dev/sde', 'size': 13 * units.Gi},
]
for i, disk in enumerate(self.disks):
disk['wwn'] = 'wwn%d' % i
disk['serial'] = 'serial%d' % i
self.inspector_client = self.app.client_manager.baremetal_introspection
self.inspector_client.data['ABCDEFGH'] = {
'inventory': {'disks': self.disks}
}
self.bm_client = self.app.client_manager.baremetal
self.bm_client.node.list.return_value = [
mock.Mock(uuid="ABCDEFGH"),
]
self.node = mock.Mock(uuid="ABCDEFGH", properties={})
self.bm_client.node.get.return_value = self.node
def test_smallest(self, find_resource_mock):
find_resource_mock.return_value = mock.Mock(id="IDIDID")
arglist = ['--root-device', 'smallest']
verifylist = [('root_device', 'smallest')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.assertEqual(self.bm_client.node.update.call_count, 2)
root_device_args = self.bm_client.node.update.call_args_list[1]
expected_patch = [{'op': 'add', 'path': '/properties/root_device',
'value': {'wwn': 'wwn2'}},
{'op': 'add', 'path': '/properties/local_gb',
'value': 4}]
self.assertEqual(mock.call('ABCDEFGH', expected_patch),
root_device_args)
def test_largest(self, find_resource_mock):
find_resource_mock.return_value = mock.Mock(id="IDIDID")
arglist = ['--root-device', 'largest']
verifylist = [('root_device', 'largest')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.assertEqual(self.bm_client.node.update.call_count, 2)
root_device_args = self.bm_client.node.update.call_args_list[1]
expected_patch = [{'op': 'add', 'path': '/properties/root_device',
'value': {'wwn': 'wwn3'}},
{'op': 'add', 'path': '/properties/local_gb',
'value': 20}]
self.assertEqual(mock.call('ABCDEFGH', expected_patch),
root_device_args)
def test_no_overwrite(self, find_resource_mock):
find_resource_mock.return_value = mock.Mock(id="IDIDID")
self.node.properties['root_device'] = {'foo': 'bar'}
arglist = ['--root-device', 'smallest']
verifylist = [('root_device', 'smallest')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.assertEqual(self.bm_client.node.update.call_count, 1)
def test_with_overwrite(self, find_resource_mock):
find_resource_mock.return_value = mock.Mock(id="IDIDID")
self.node.properties['root_device'] = {'foo': 'bar'}
arglist = ['--root-device', 'smallest',
'--overwrite-root-device-hints']
verifylist = [('root_device', 'smallest'),
('overwrite_root_device_hints', True)]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.assertEqual(self.bm_client.node.update.call_count, 2)
root_device_args = self.bm_client.node.update.call_args_list[1]
expected_patch = [{'op': 'add', 'path': '/properties/root_device',
'value': {'wwn': 'wwn2'}},
{'op': 'add', 'path': '/properties/local_gb',
'value': 4}]
self.assertEqual(mock.call('ABCDEFGH', expected_patch),
root_device_args)
def test_minimum_size(self, find_resource_mock):
find_resource_mock.return_value = mock.Mock(id="IDIDID")
arglist = ['--root-device', 'smallest',
'--root-device-minimum-size', '10']
verifylist = [('root_device', 'smallest')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.assertEqual(self.bm_client.node.update.call_count, 2)
root_device_args = self.bm_client.node.update.call_args_list[1]
expected_patch = [{'op': 'add', 'path': '/properties/root_device',
'value': {'wwn': 'wwn0'}},
{'op': 'add', 'path': '/properties/local_gb',
'value': 10}]
self.assertEqual(mock.call('ABCDEFGH', expected_patch),
root_device_args)
def test_bad_inventory(self, find_resource_mock):
find_resource_mock.return_value = mock.Mock(id="IDIDID")
del self.inspector_client.data['ABCDEFGH']['inventory']
arglist = ['--root-device', 'smallest']
verifylist = [('root_device', 'smallest')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.assertRaisesRegexp(exceptions.RootDeviceDetectionError,
"Malformed introspection data",
self.cmd.take_action, parsed_args)
self.assertEqual(self.bm_client.node.update.call_count, 1)
def test_no_disks(self, find_resource_mock):
find_resource_mock.return_value = mock.Mock(id="IDIDID")
self.inspector_client.data['ABCDEFGH']['inventory']['disks'] = [
{'name': '/dev/sda', 'size': 1 * units.Gi}
]
arglist = ['--root-device', 'smallest']
verifylist = [('root_device', 'smallest')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.assertRaisesRegexp(exceptions.RootDeviceDetectionError,
"No suitable disks",
self.cmd.take_action, parsed_args)
self.assertEqual(self.bm_client.node.update.call_count, 1)
def test_no_data(self, find_resource_mock):
find_resource_mock.return_value = mock.Mock(id="IDIDID")
del self.inspector_client.data['ABCDEFGH']
arglist = ['--root-device', 'smallest']
verifylist = [('root_device', 'smallest')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.assertRaisesRegexp(exceptions.RootDeviceDetectionError,
"No introspection data",
self.cmd.take_action, parsed_args)
self.assertEqual(self.bm_client.node.update.call_count, 1)
def test_no_wwn_and_serial(self, find_resource_mock):
find_resource_mock.return_value = mock.Mock(id="IDIDID")
self.inspector_client.data['ABCDEFGH']['inventory']['disks'] = [
{'name': '/dev/sda', 'size': 10 * units.Gi}
]
arglist = ['--root-device', 'smallest']
verifylist = [('root_device', 'smallest')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.assertRaisesRegexp(exceptions.RootDeviceDetectionError,
"Neither WWN nor serial number are known",
self.cmd.take_action, parsed_args)
self.assertEqual(self.bm_client.node.update.call_count, 1)
def test_device_list(self, find_resource_mock):
find_resource_mock.return_value = mock.Mock(id="IDIDID")
arglist = ['--root-device', 'hda,sda,sdb,sdc']
verifylist = [('root_device', 'hda,sda,sdb,sdc')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.assertEqual(self.bm_client.node.update.call_count, 2)
root_device_args = self.bm_client.node.update.call_args_list[1]
expected_patch = [{'op': 'add', 'path': '/properties/root_device',
'value': {'wwn': 'wwn0'}},
{'op': 'add', 'path': '/properties/local_gb',
'value': 10}]
self.assertEqual(mock.call('ABCDEFGH', expected_patch),
root_device_args)
def test_device_list_not_found(self, find_resource_mock):
find_resource_mock.return_value = mock.Mock(id="IDIDID")
arglist = ['--root-device', 'hda']
verifylist = [('root_device', 'hda')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.assertRaisesRegexp(exceptions.RootDeviceDetectionError,
"Cannot find a disk",
self.cmd.take_action, parsed_args)
self.assertEqual(self.bm_client.node.update.call_count, 1)
class TestShowNodeCapabilities(fakes.TestBaremetal):
def setUp(self):

View File

@ -24,8 +24,10 @@ import yaml
from cliff import command
from cliff import lister
import ironic_inspector_client
from openstackclient.common import utils as osc_utils
from openstackclient.i18n import _
from oslo_utils import units
from tripleo_common.utils import nodes
from tripleoclient import exceptions
@ -438,6 +440,20 @@ class ConfigureBaremetalBoot(command.Command):
parser.add_argument('--deploy-ramdisk',
default='bm-deploy-ramdisk',
help='Image with deploy ramdisk.')
parser.add_argument('--root-device',
help='Define the root device for nodes. '
'Can be either a list of device names (without '
'/dev) to choose from or one of two strategies: '
'largest or smallest. For it to work this command '
'should be run after the introspection.')
parser.add_argument('--root-device-minimum-size',
type=int, default=4,
help='Minimum size (in GiB) of the detected '
'root device. Used with --detect-root-device.')
parser.add_argument('--overwrite-root-device-hints',
action='store_true',
help='Whether to overwrite existing root device '
'hints when --detect-root-device is used.')
return parser
def take_action(self, parsed_args):
@ -521,6 +537,106 @@ class ConfigureBaremetalBoot(command.Command):
},
])
self._apply_root_device_strategy(
node_detail, parsed_args.root_device,
parsed_args.root_device_minimum_size,
parsed_args.overwrite_root_device_hints)
def _apply_root_device_strategy(self, node, strategy, minimum_size,
overwrite=False):
if not strategy:
return
if node.properties.get('root_device') and not overwrite:
# This is a correct situation, we still want to allow people to
# fine-tune the root device setting for a subset of nodes.
# However, issue a warning, so that they know which nodes were not
# updated during this run.
self.log.warning('Root device hints are already set for node %s '
'and overwriting is not requested, skipping',
node.uuid)
self.log.warning('You may unset them by running $ ironic '
'node-update %s remove properties/root_device',
node.uuid)
return
inspector_client = self.app.client_manager.baremetal_introspection
try:
data = inspector_client.get_data(node.uuid)
except ironic_inspector_client.ClientError:
raise exceptions.RootDeviceDetectionError(
'No introspection data found for node %s, '
'root device cannot be detected' % node.uuid)
except AttributeError:
raise RuntimeError('Ironic inspector client version 1.2.0 or '
'newer is required for detecting root device')
try:
disks = data['inventory']['disks']
except KeyError:
raise exceptions.RootDeviceDetectionError(
'Malformed introspection data for node %s: '
'disks list is missing' % node.uuid)
minimum_size *= units.Gi
disks = [d for d in disks if d.get('size', 0) >= minimum_size]
if not disks:
raise exceptions.RootDeviceDetectionError(
'No suitable disks found for node %s' % node.uuid)
if strategy == 'smallest':
disks.sort(key=lambda d: d['size'])
root_device = disks[0]
elif strategy == 'largest':
disks.sort(key=lambda d: d['size'], reverse=True)
root_device = disks[0]
else:
disk_names = [x.strip() for x in strategy.split(',')]
disks = {d['name']: d for d in disks}
for candidate in disk_names:
try:
root_device = disks['/dev/%s' % candidate]
except KeyError:
continue
else:
break
else:
raise exceptions.RootDeviceDetectionError(
'Cannot find a disk with any of names %(strategy)s '
'for node %(node)s' %
{'strategy': strategy, 'node': node.uuid})
hint = None
for hint_name in ('wwn', 'serial'):
if root_device.get(hint_name):
hint = {hint_name: root_device[hint_name]}
break
if hint is None:
# I don't think it might actually happen, but just in case
raise exceptions.RootDeviceDetectionError(
'Neither WWN nor serial number are known for device %(dev)s '
'on node %(node)s; root device hints cannot be used' %
{'dev': root_device['name'], 'node': node.uuid})
# During the introspection process we got local_gb assigned according
# to the default strategy. Now we need to update it.
new_size = root_device['size'] / units.Gi
# This -1 is what we always do to account for partitioning
new_size -= 1
bm_client = self.app.client_manager.baremetal
bm_client.node.update(
node.uuid,
[{'op': 'add', 'path': '/properties/root_device', 'value': hint},
{'op': 'add', 'path': '/properties/local_gb', 'value': new_size}])
self.log.info('Updated root device for node %(node)s, new device '
'is %(dev)s, new local_gb is %(local_gb)d',
{'node': node.uuid, 'dev': root_device,
'local_gb': new_size})
class ShowNodeCapabilities(lister.Lister):
"""List the capabilities for all Nodes"""