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:
parent
de0fcbfcbd
commit
600c7df819
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue