Don't use mistral actions for baremetal workflow

This refactors and moves the code to tripleoclient
for us to be able to remove dependency on mistral-lib.

Change-Id: I62352871311e98927fbd560b4235114c8c62f223
This commit is contained in:
ramishra 2021-03-01 11:59:49 +05:30
parent de0fcbfcbd
commit 600c7df819
5 changed files with 660 additions and 87 deletions

View File

@ -60,6 +60,10 @@ class RootUserExecution(Base):
"""Command was executed by a root user"""
class RootDeviceDetectionError(Base):
"""Failed to detect the root device"""
class InvalidConfiguration(Base, ValueError):
"""Invalid parameters were specified for the deployment"""

View File

@ -149,8 +149,10 @@ class FakePlaybookExecution(utils.TestCommand):
self.app.options = FakeOptions()
self.app.client_manager.auth_ref = mock.Mock(auth_token="TOKEN")
baremetal = self.app.client_manager.baremetal = mock.Mock()
baremetal.node.list.return_value = []
self.baremetal = self.app.client_manager.baremetal = mock.MagicMock()
self.app.client_manager.baremetal_introspection = mock.MagicMock()
self.inspector = self.app.client_manager.baremetal_introspection
self.baremetal.node.list.return_value = []
compute = self.app.client_manager.compute = mock.Mock()
compute.servers.list.return_value = []
self.app.client_manager.identity = mock.Mock()
@ -169,25 +171,12 @@ class FakePlaybookExecution(utils.TestCommand):
self.addCleanup(get_key.stop)
self.register_or_update = mock.patch(
'tripleo_common.actions.baremetal.RegisterOrUpdateNodes.run',
'tripleoclient.workflows.baremetal.register_or_update',
autospec=True,
return_value=[mock.Mock(uuid='MOCK_NODE_UUID')]
)
self.register_or_update.start()
self.addCleanup(self.register_or_update.stop)
self.boot_action = mock.patch(
'tripleo_common.actions.baremetal.ConfigureBootAction.run',
autospec=True,
return_value=None
)
self.boot_action.start()
self.addCleanup(self.boot_action.stop)
self.boot_action = mock.patch(
'tripleo_common.actions.baremetal.ConfigureRootDeviceAction.run',
autospec=True
)
self.boot_action.start()
self.addCleanup(self.boot_action.stop)
if ansible_mock:
get_stack = mock.patch('tripleoclient.utils.get_stack')

View File

@ -602,7 +602,6 @@ class TestConfigureNode(fakes.TestOvercloudNode):
self.cmd = overcloud_node.ConfigureNode(self.app, None)
self.http_boot = '/var/lib/ironic/httpboot'
self.workflow_input = {
'kernel_name': 'file://%s/agent.kernel' % self.http_boot,
'ramdisk_name': 'file://%s/agent.ramdisk' % self.http_boot,
@ -666,7 +665,10 @@ class TestConfigureNode(fakes.TestOvercloudNode):
parsed_args = self.check_parser(self.cmd, argslist, verifylist)
self.cmd.take_action(parsed_args)
def test_configure_specified_node_with_all_arguments(self):
@mock.patch('tripleoclient.workflows.baremetal.'
'_apply_root_device_strategy')
def test_configure_specified_node_with_all_arguments(
self, mock_root_device):
argslist = ['node_id',
'--deploy-kernel', 'test_kernel',
'--deploy-ramdisk', 'test_ramdisk',
@ -699,7 +701,7 @@ class TestDiscoverNode(fakes.TestOvercloudNode):
self.cmd = overcloud_node.DiscoverNode(self.app, None)
self.gcn = mock.patch(
'tripleo_common.actions.baremetal.GetCandidateNodes',
'tripleoclient.workflows.baremetal._get_candidate_nodes',
autospec=True
)
self.gcn.start()

View File

@ -13,7 +13,13 @@
# under the License.
import mock
import netaddr
import ironic_inspector_client
from oslo_concurrency import processutils
from oslo_utils import units
from tripleoclient import exceptions
from tripleoclient.tests import fakes
from tripleoclient.workflows import baremetal
@ -22,11 +28,10 @@ class TestBaremetalWorkflows(fakes.FakePlaybookExecution):
def setUp(self):
super(TestBaremetalWorkflows, self).setUp()
self.app.client_manager.workflow_engine = self.workflow = mock.Mock()
self.glance = self.app.client_manager.image = mock.Mock()
self.tripleoclient = mock.Mock()
self.app.client_manager.tripleoclient = self.tripleoclient
self.mock_playbook = mock.patch(
'tripleoclient.utils.run_ansible_playbook',
autospec=True
@ -34,6 +39,56 @@ class TestBaremetalWorkflows(fakes.FakePlaybookExecution):
self.mock_playbook.start()
self.addCleanup(self.mock_playbook.stop)
self.node_update = [{'op': 'add',
'path': '/properties/capabilities',
'value': 'boot_option:local'},
{'op': 'add',
'path': '/driver_info/deploy_ramdisk',
'value': None},
{'op': 'add',
'path': '/driver_info/deploy_kernel',
'value': None},
{'op': 'add',
'path': '/driver_info/rescue_ramdisk',
'value': None},
{'op': 'add',
'path': '/driver_info/rescue_kernel',
'value': None}]
# Mock data
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.baremetal.node.list.return_value = [
mock.Mock(uuid="ABCDEFGH"),
]
self.node = mock.Mock(uuid="ABCDEFGH", properties={})
self.baremetal.node.get.return_value = self.node
self.inspector.get_data.return_value = {
'inventory': {'disks': self.disks}
}
self.existing_nodes = [
{'uuid': '1', 'driver': 'ipmi',
'driver_info': {'ipmi_address': '10.0.0.1'}},
{'uuid': '2', 'driver': 'pxe_ipmitool',
'driver_info': {'ipmi_address': '10.0.0.1', 'ipmi_port': 6235}},
{'uuid': '3', 'driver': 'foobar', 'driver_info': {}},
{'uuid': '4', 'driver': 'fake',
'driver_info': {'fake_address': 42}},
{'uuid': '5', 'driver': 'ipmi', 'driver_info': {}},
{'uuid': '6', 'driver': 'pxe_drac',
'driver_info': {'drac_address': '10.0.0.2'}},
{'uuid': '7', 'driver': 'pxe_drac',
'driver_info': {'drac_address': '10.0.0.3', 'drac_port': 6230}},
]
def test_register_or_update_success(self):
self.assertEqual(baremetal.register_or_update(
self.app.client_manager,
@ -44,13 +99,6 @@ class TestBaremetalWorkflows(fakes.FakePlaybookExecution):
def test_provide_success(self):
baremetal.provide(self.app.client_manager, node_uuids=[])
def test_format_errors(self):
payload = {'message': [{'result': 'Error1a\nError1b'},
{'result': 'Error2a\nError2b\n'}]}
error_string = baremetal._format_errors(payload)
self.assertEqual(error_string, "Error1b\nError2b")
def test_introspect_success(self):
baremetal.introspect(self.app.client_manager, node_uuids=[],
run_validations=True, concurrency=20,
@ -81,3 +129,310 @@ class TestBaremetalWorkflows(fakes.FakePlaybookExecution):
baremetal.clean_manageable_nodes(
self.app.client_manager
)
def test_run_instance_boot_option(self):
result = baremetal._configure_boot(
self.app.client_manager,
node_uuid='MOCK_UUID',
instance_boot_option='netboot')
self.assertIsNone(result)
self.node_update[0].update({'value': 'boot_option:netboot'})
self.baremetal.node.update.assert_called_once_with(
mock.ANY, self.node_update)
def test_run_instance_boot_option_not_set(self):
result = baremetal._configure_boot(
self.app.client_manager,
node_uuid='MOCK_UUID')
self.assertIsNone(result)
self.node_update[0].update({'value': ''})
self.baremetal.node.update.assert_called_once_with(
mock.ANY, self.node_update)
def test_run_instance_boot_option_already_set_no_overwrite(self):
node_mock = mock.MagicMock()
node_mock.properties.get.return_value = ({'boot_option': 'netboot'})
self.app.client_manager.baremetal.node.get.return_value = node_mock
result = baremetal._configure_boot(
self.app.client_manager,
node_uuid='MOCK_UUID')
self.assertIsNone(result)
self.node_update[0].update({'value': 'boot_option:netboot'})
self.baremetal.node.update.assert_called_once_with(
mock.ANY, self.node_update)
def test_run_instance_boot_option_already_set_do_overwrite(self):
node_mock = mock.MagicMock()
node_mock.properties.get.return_value = ({'boot_option': 'netboot'})
self.app.client_manager.baremetal.node.get.return_value = node_mock
result = baremetal._configure_boot(
self.app.client_manager,
node_uuid='MOCK_UUID',
instance_boot_option='local')
self.assertIsNone(result)
self.node_update[0].update({'value': 'boot_option:local'})
self.baremetal.node.update.assert_called_once_with(
mock.ANY, self.node_update)
def test_run_exception_on_node_update(self):
self.baremetal.node.update.side_effect = Exception("Update error")
self.assertRaises(
Exception,
baremetal._configure_boot,
self.app.client_manager,
node_uuid='MOCK_UUID')
self.inspector.get_data.return_value = {
'inventory': {'disks': self.disks}
}
def test_smallest(self):
baremetal._apply_root_device_strategy(
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='smallest')
self.assertEqual(self.baremetal.node.update.call_count, 1)
root_device_args = self.baremetal.node.update.call_args_list[0]
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_smallest_with_ext(self):
self.disks[2]['wwn_with_extension'] = 'wwnext'
baremetal._apply_root_device_strategy(
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='smallest')
self.assertEqual(self.baremetal.node.update.call_count, 1)
root_device_args = self.baremetal.node.update.call_args_list[0]
expected_patch = [{'op': 'add', 'path': '/properties/root_device',
'value': {'wwn_with_extension': 'wwnext'}},
{'op': 'add', 'path': '/properties/local_gb',
'value': 4}]
self.assertEqual(mock.call('ABCDEFGH', expected_patch),
root_device_args)
def test_largest(self):
baremetal._apply_root_device_strategy(
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='largest')
self.assertEqual(self.baremetal.node.update.call_count, 1)
root_device_args = self.baremetal.node.update.call_args_list[0]
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_largest_with_ext(self):
self.disks[3]['wwn_with_extension'] = 'wwnext'
baremetal._apply_root_device_strategy(
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='largest')
self.assertEqual(self.baremetal.node.update.call_count, 1)
root_device_args = self.baremetal.node.update.call_args_list[0]
expected_patch = [{'op': 'add', 'path': '/properties/root_device',
'value': {'wwn_with_extension': 'wwnext'}},
{'op': 'add', 'path': '/properties/local_gb',
'value': 20}]
self.assertEqual(mock.call('ABCDEFGH', expected_patch),
root_device_args)
def test_no_overwrite(self):
self.node.properties['root_device'] = {'foo': 'bar'}
baremetal._apply_root_device_strategy(
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='smallest')
self.assertEqual(self.baremetal.node.update.call_count, 0)
def test_with_overwrite(self):
self.node.properties['root_device'] = {'foo': 'bar'}
baremetal._apply_root_device_strategy(
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='smallest',
overwrite=True)
self.assertEqual(self.baremetal.node.update.call_count, 1)
root_device_args = self.baremetal.node.update.call_args_list[0]
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):
baremetal._apply_root_device_strategy(
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='smallest',
minimum_size=10)
self.assertEqual(self.baremetal.node.update.call_count, 1)
root_device_args = self.baremetal.node.update.call_args_list[0]
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):
self.inspector.get_data.return_value = {}
self.assertRaisesRegex(exceptions.RootDeviceDetectionError,
"Malformed introspection data",
baremetal._apply_root_device_strategy,
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='smallest')
self.assertEqual(self.baremetal.node.update.call_count, 0)
def test_no_disks(self):
self.inspector.get_data.return_value = {
'inventory': {
'disks': [{'name': '/dev/sda', 'size': 1 * units.Gi}]
}
}
self.assertRaisesRegex(exceptions.RootDeviceDetectionError,
"No suitable disks",
baremetal._apply_root_device_strategy,
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='smallest')
self.assertEqual(self.baremetal.node.update.call_count, 0)
def test_no_data(self):
self.inspector.get_data.side_effect = (
ironic_inspector_client.ClientError(mock.Mock()))
self.assertRaisesRegex(exceptions.RootDeviceDetectionError,
"No introspection data",
baremetal._apply_root_device_strategy,
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='smallest')
self.assertEqual(self.baremetal.node.update.call_count, 0)
def test_no_wwn_and_serial(self):
self.inspector.get_data.return_value = {
'inventory': {
'disks': [{'name': '/dev/sda', 'size': 10 * units.Gi}]
}
}
self.assertRaisesRegex(exceptions.RootDeviceDetectionError,
"Neither WWN nor serial number are known",
baremetal._apply_root_device_strategy,
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='smallest')
self.assertEqual(self.baremetal.node.update.call_count, 0)
def test_device_list(self):
baremetal._apply_root_device_strategy(
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='hda,sda,sdb,sdc')
self.assertEqual(self.baremetal.node.update.call_count, 1)
root_device_args = self.baremetal.node.update.call_args_list[0]
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):
self.assertRaisesRegex(exceptions.RootDeviceDetectionError,
"Cannot find a disk",
baremetal._apply_root_device_strategy,
self.app.client_manager,
node_uuid='MOCK_UUID',
strategy='hda')
self.assertEqual(self.baremetal.node.update.call_count, 0)
def test_existing_ips(self):
result = baremetal._existing_ips(self.existing_nodes)
self.assertEqual({('10.0.0.1', 623), ('10.0.0.1', 6235),
('10.0.0.2', None), ('10.0.0.3', 6230)},
set(result))
def test_with_list(self):
result = baremetal._get_candidate_nodes(
['10.0.0.1', '10.0.0.2', '10.0.0.3'],
[623, 6230, 6235],
[['admin', 'password'], ['admin', 'admin']],
self.existing_nodes)
self.assertEqual([
{'ip': '10.0.0.3', 'port': 623,
'username': 'admin', 'password': 'password'},
{'ip': '10.0.0.1', 'port': 6230,
'username': 'admin', 'password': 'password'},
{'ip': '10.0.0.3', 'port': 6235,
'username': 'admin', 'password': 'password'},
{'ip': '10.0.0.3', 'port': 623,
'username': 'admin', 'password': 'admin'},
{'ip': '10.0.0.1', 'port': 6230,
'username': 'admin', 'password': 'admin'},
{'ip': '10.0.0.3', 'port': 6235,
'username': 'admin', 'password': 'admin'},
], result)
def test_with_subnet(self):
result = baremetal._get_candidate_nodes(
'10.0.0.0/30',
[623, 6230, 6235],
[['admin', 'password'], ['admin', 'admin']],
self.existing_nodes)
self.assertEqual([
{'ip': '10.0.0.1', 'port': 6230,
'username': 'admin', 'password': 'password'},
{'ip': '10.0.0.1', 'port': 6230,
'username': 'admin', 'password': 'admin'},
], result)
def test_invalid_subnet(self):
self.assertRaises(
netaddr.core.AddrFormatError,
baremetal._get_candidate_nodes,
'meow',
[623, 6230, 6235],
[['admin', 'password'], ['admin', 'admin']],
self.existing_nodes)
@mock.patch.object(processutils, 'execute', autospec=True)
def test_success(self, mock_execute):
result = baremetal._probe_node('10.0.0.42', 623,
'admin', 'password')
self.assertEqual({'pm_type': 'ipmi',
'pm_addr': '10.0.0.42',
'pm_user': 'admin',
'pm_password': 'password',
'pm_port': 623},
result)
mock_execute.assert_called_once_with('ipmitool', '-I', 'lanplus',
'-H', '10.0.0.42',
'-L', 'ADMINISTRATOR',
'-p', '623', '-U', 'admin',
'-f', mock.ANY, 'power', 'status',
attempts=2)
@mock.patch.object(processutils, 'execute', autospec=True)
def test_failure(self, mock_execute):
mock_execute.side_effect = processutils.ProcessExecutionError()
self.assertIsNone(baremetal._probe_node('10.0.0.42', 623,
'admin', 'password'))
mock_execute.assert_called_once_with('ipmitool', '-I', 'lanplus',
'-H', '10.0.0.42',
'-L', 'ADMINISTRATOR',
'-p', '623', '-U', 'admin',
'-f', mock.ANY, 'power', 'status',
attempts=2)

View File

@ -12,14 +12,23 @@
# License for the specific language governing permissions and limitations
# under the License.
import six
import logging
import socket
import netaddr
import tempfile
from tripleo_common.actions import baremetal
import ironic_inspector_client
from oslo_concurrency import processutils
from oslo_utils import units
from tripleo_common import exception as tc_exceptions
from tripleo_common.utils import nodes as node_utils
from tripleoclient import constants
from tripleoclient import exceptions
from tripleoclient import utils
LOG = logging.getLogger(__name__)
def validate_nodes(clients, nodes_json):
"""Validate nodes.
@ -32,10 +41,8 @@ def validate_nodes(clients, nodes_json):
:returns: Boolean
"""
context = clients.tripleoclient.create_mistral_context()
nodes = baremetal.ValidateNodes(nodes_json=nodes_json)
validated_nodes = nodes.run(context=context)
nodes_json = node_utils.convert_nodes_json_mac_to_ports(nodes_json)
validated_nodes = node_utils.validate_nodes(nodes_json)
if not validated_nodes:
return True
else:
@ -66,15 +73,19 @@ def register_or_update(clients, nodes_json, kernel_name=None,
:returns: List
"""
context = clients.tripleoclient.create_mistral_context()
nodes = baremetal.RegisterOrUpdateNodes(
nodes_json=nodes_json,
ramdisk_name=ramdisk_name,
kernel_name=kernel_name,
instance_boot_option=instance_boot_option
)
nodes_json = node_utils.convert_nodes_json_mac_to_ports(nodes_json)
for node in nodes_json:
caps = node.get('capabilities', {})
caps = node_utils.capabilities_to_dict(caps)
if instance_boot_option is not None:
caps.setdefault('boot_option', instance_boot_option)
node['capabilities'] = node_utils.dict_to_capabilities(caps)
registered_nodes = nodes.run(context=context)
registered_nodes = node_utils.register_all_nodes(
nodes_json,
client=clients.baremetal,
kernel_name=kernel_name,
ramdisk_name=ramdisk_name)
if not isinstance(registered_nodes, list):
raise exceptions.RegisterOrUpdateError(registered_nodes)
else:
@ -91,26 +102,6 @@ def register_or_update(clients, nodes_json, kernel_name=None,
return registered_nodes
def _format_errors(payload):
errors = []
messages = payload.get('message', [])
for msg in messages:
# Adapt for different formats
if isinstance(msg, six.string_types):
text = msg
else:
text = msg.get('result') or msg.get('message', '')
try:
# With multiple workflows, the error message can become
# quite large and unreadable as it gets passed from task to
# task. This attempts to keep only the last, and hopefully
# useful part.
errors.append(text.rstrip('\n').split('\n')[-1])
except Exception:
errors.append(text)
return '\n'.join(errors)
def provide(verbosity, node_uuids):
"""Provide Baremetal Nodes
@ -238,6 +229,141 @@ def introspect_manageable_nodes(clients, run_validations, concurrency,
)
def _configure_boot(clients, node_uuid,
kernel_name='bm-deploy-kernel',
ramdisk_name='bm-deploy-ramdisk',
instance_boot_option=None):
baremetal_client = clients.baremetal
image_ids = {'kernel': None, 'ramdisk': None}
node = baremetal_client.node.get(node_uuid)
capabilities = node.properties.get('capabilities', {})
capabilities = node_utils.capabilities_to_dict(capabilities)
if instance_boot_option is not None:
capabilities['boot_option'] = instance_boot_option
capabilities = node_utils.dict_to_capabilities(capabilities)
baremetal_client.node.update(node.uuid, [
{
'op': 'add',
'path': '/properties/capabilities',
'value': capabilities,
},
{
'op': 'add',
'path': '/driver_info/deploy_ramdisk',
'value': image_ids['ramdisk'],
},
{
'op': 'add',
'path': '/driver_info/deploy_kernel',
'value': image_ids['kernel'],
},
{
'op': 'add',
'path': '/driver_info/rescue_ramdisk',
'value': image_ids['ramdisk'],
},
{
'op': 'add',
'path': '/driver_info/rescue_kernel',
'value': image_ids['kernel'],
},
])
def _apply_root_device_strategy(clients, node_uuid, strategy,
minimum_size=4, overwrite=False):
node = clients.baremetal.node.get(node_uuid)
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.
LOG.warning('Root device hints are already set for node %s '
'and overwriting is not requested, skipping',
node.uuid)
LOG.warning('You may unset them by running $ ironic '
'node-update %s remove properties/root_device',
node.uuid)
return
inspector_client = clients.baremetal_introspection
baremetal_client = clients.baremetal
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_with_extension', '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
baremetal_client.node.update(
node.uuid,
[{'op': 'add', 'path': '/properties/root_device', 'value': hint},
{'op': 'add', 'path': '/properties/local_gb', 'value': new_size}])
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})
def configure(clients, node_uuids, kernel_name='bm-deploy-kernel',
ramdisk_name='bm-deploy-ramdisk', instance_boot_option=None,
root_device=None, root_device_minimum_size=4,
@ -268,25 +394,16 @@ def configure(clients, node_uuids, kernel_name='bm-deploy-kernel',
:type overwrite_root_device_hints: Boolean
"""
context = clients.tripleoclient.create_mistral_context()
for node_uuid in node_uuids:
boot_action = baremetal.ConfigureBootAction(
node_uuid=node_uuid,
kernel_name=kernel_name,
ramdisk_name=ramdisk_name,
instance_boot_option=instance_boot_option
).run(context=context)
if boot_action:
raise RuntimeError(boot_action)
root_device_action = baremetal.ConfigureRootDeviceAction(
node_uuid=node_uuid,
root_device=root_device,
minimum_size=root_device_minimum_size,
overwrite=overwrite_root_device_hints
)
root_device_action.run(context=context)
else:
print('Successfully configured the nodes.')
_configure_boot(clients, node_uuid, kernel_name,
ramdisk_name, instance_boot_option)
if root_device:
_apply_root_device_strategy(
clients, node_uuid,
strategy=root_device,
minimum_size=root_device_minimum_size,
overwrite=overwrite_root_device_hints)
print('Successfully configured the nodes.')
def configure_manageable_nodes(clients, kernel_name='bm-deploy-kernel',
@ -372,6 +489,116 @@ def create_raid_configuration(clients, node_uuids, configuration,
print('Successfully configured RAID for nodes: {}'.format(node_uuids))
def _existing_ips(existing_nodes):
result = set()
for node in existing_nodes:
try:
handler = node_utils.find_driver_handler(node['driver'])
except tc_exceptions.InvalidNode:
LOG.warning('No known handler for driver %(driver)s of '
'node %(node)s, ignoring it',
{'driver': node['driver'], 'node': node['uuid']})
continue
address_field = handler.convert_key('pm_addr')
if address_field is None:
LOG.info('No address field for driver %(driver)s of '
'node %(node)s, ignoring it',
{'driver': node['driver'], 'node': node['uuid']})
continue
address = node['driver_info'].get(address_field)
if address is None:
LOG.warning('No address for node %(node)s, ignoring it',
{'node': node['uuid']})
continue
try:
ip = socket.gethostbyname(address)
except socket.gaierror as exc:
LOG.warning('Cannot resolve %(field)s "%(value)s" '
'for node %(node)s: %(error)s',
{'field': address_field, 'value': address,
'node': node['uuid'], 'error': exc})
continue
port_field = handler.convert_key('pm_port')
port = node['driver_info'].get(port_field, handler.default_port)
if port is not None:
port = int(port)
LOG.debug('Detected existing BMC at %s with port %s', ip, port)
result.add((ip, port))
return result
def _ip_address_list(ip_addresses):
if isinstance(ip_addresses, str):
return [str(ip) for ip in
netaddr.IPNetwork(ip_addresses).iter_hosts()]
return ip_addresses
def _get_candidate_nodes(ip_addresses, ports,
credentials, existing_nodes):
existing = _existing_ips(existing_nodes)
try:
ip_addresses = _ip_address_list(ip_addresses)
except netaddr.AddrFormatError as exc:
LOG.error("Cannot parse network address: %s", exc)
raise
result = []
# NOTE(dtantsur): we iterate over IP addresses last to avoid
# spamming the same BMC with too many requests in a row.
for username, password in credentials:
for port in ports:
port = int(port)
for ip in ip_addresses:
if (ip, port) in existing or (ip, None) in existing:
LOG.info('Skipping existing node %s:%s', ip, port)
continue
result.append({'ip': ip, 'username': username,
'password': password, 'port': port})
return result
def _probe_node(ip, port, username, password,
attempts=2, ipmi_driver='ipmi'):
# TODO(dtantsur): redfish support
LOG.debug('Probing for IPMI BMC: %s@%s:%s',
username, ip, port)
with tempfile.NamedTemporaryFile(mode='wt') as fp:
fp.write(password or '\0')
fp.flush()
try:
# TODO(dtantsur): try also IPMI v1.5
processutils.execute('ipmitool', '-I', 'lanplus',
'-H', ip, '-L', 'ADMINISTRATOR',
'-p', str(port), '-U', username,
'-f', fp.name, 'power', 'status',
attempts=attempts)
except processutils.ProcessExecutionError as exc:
LOG.debug('Probing %(ip)s failed: %(exc)s',
{'ip': ip, 'exc': exc})
return None
LOG.info('Found a BMC on %(ip)s with user %(user)s',
{'ip': ip, 'user': username})
return {
'pm_type': ipmi_driver,
'pm_addr': ip,
'pm_user': username,
'pm_password': password,
'pm_port': port,
}
def discover_and_enroll(clients, ip_addresses, credentials, kernel_name,
ramdisk_name, instance_boot_option,
existing_nodes=None, ports=None):
@ -413,19 +640,15 @@ def discover_and_enroll(clients, ip_addresses, credentials, kernel_name,
if not existing_nodes:
existing_nodes = list()
context = clients.tripleoclient.create_mistral_context()
get_candiate_nodes = baremetal.GetCandidateNodes(
candidate_nodes = _get_candidate_nodes(
ip_addresses,
ports,
credentials,
existing_nodes
)
probed_nodes = list()
for node in get_candiate_nodes.run(context=context):
probed_nodes.append(
baremetal.ProbeNode(**node).run(context=context)
)
for node in candidate_nodes:
probed_nodes.append(_probe_node(**node))
print('Successfully probed node IP {}'.format(node['ip']))
return register_or_update(