Move baremetal configure commands from Ansible

Currently, we leverage Ansible to handle the
baremetal configure process. As part of our efforts
to support work on Tripleo.Next, these Ansible
workflows will need to be migrated to tripleoclient.

This change is consolodating Python methods from
tripleo-ansible into tripleoclient.

Change-Id: I1dbb3e4864688ec931bb19a8f1891d2822632f5a
This commit is contained in:
Brendan Shephard 2022-01-06 12:52:54 +00:00
parent 75b27c6a9d
commit 134270f240
5 changed files with 339 additions and 136 deletions

View File

@ -25,6 +25,7 @@ import openstack
from osc_lib import exceptions as oscexc
from osc_lib.tests import utils as test_utils
from oslo_utils import units
import yaml
from tripleoclient import exceptions
@ -697,6 +698,14 @@ class TestImportNodeMultiArch(fakes.TestOvercloudNode):
self._check_workflow_call(parsed_args, no_deploy_image=True)
@mock.patch.object(openstack.baremetal.v1._proxy, 'Proxy',
autospec=True, name='mock_bm')
@mock.patch('openstack.config', autospec=True,
name='mock_conf')
@mock.patch('openstack.connect', autospec=True,
name='mock_connect')
@mock.patch.object(openstack.connection,
'Connection', autospec=True)
class TestConfigureNode(fakes.TestOvercloudNode):
def setUp(self):
@ -714,26 +723,62 @@ class TestConfigureNode(fakes.TestOvercloudNode):
'root_device_minimum_size': 4,
'overwrite_root_device_hints': False
}
# Mock disks
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},
]
def test_configure_all_manageable_nodes(self):
for i, disk in enumerate(self.disks):
disk['wwn'] = 'wwn%d' % i
disk['serial'] = 'serial%d' % i
self.fake_baremetal_node = fakes.make_fake_machine(
machine_name='node1',
machine_id='4e540e11-1366-4b57-85d5-319d168d98a1'
)
self.fake_baremetal_node2 = fakes.make_fake_machine(
machine_name='node2',
machine_id='9070e42d-1ad7-4bd0-b868-5418bc9c7176'
)
def test_configure_all_manageable_nodes(self, mock_conn,
mock_connect, mock_conf,
mock_bm):
mock_conn.return_value = mock_bm
mock_bm.baremetal = mock_bm
mock_bm.baremetal.nodes.side_effect = [
iter([self.fake_baremetal_node]),
iter([self.fake_baremetal_node])]
parsed_args = self.check_parser(self.cmd,
['--all-manageable'],
[('all_manageable', True)])
self.cmd.take_action(parsed_args)
def test_configure_specified_nodes(self):
def test_configure_specified_nodes(self, mock_conn,
mock_connect, mock_conf,
mock_bm):
mock_conn.return_value = mock_bm
mock_bm.baremetal = mock_bm
argslist = ['node_uuid1', 'node_uuid2']
verifylist = [('node_uuids', ['node_uuid1', 'node_uuid2'])]
parsed_args = self.check_parser(self.cmd, argslist, verifylist)
self.cmd.take_action(parsed_args)
def test_configure_no_node_or_flag_specified(self):
def test_configure_no_node_or_flag_specified(self, mock_conn,
mock_connect, mock_conf,
mock_bm):
self.assertRaises(test_utils.ParserException,
self.check_parser,
self.cmd, [], [])
def test_configure_uuids_and_all_both_specified(self):
def test_configure_uuids_and_all_both_specified(self, mock_conn,
mock_connect, mock_conf,
mock_bm):
argslist = ['node_id1', 'node_id2', '--all-manageable']
verifylist = [('node_uuids', ['node_id1', 'node_id2']),
('all_manageable', True)]
@ -741,7 +786,23 @@ class TestConfigureNode(fakes.TestOvercloudNode):
self.check_parser,
self.cmd, argslist, verifylist)
def test_configure_kernel_and_ram(self):
def test_configure_kernel_and_ram(self, mock_conn,
mock_connect, mock_conf,
mock_bm):
mock_conn.return_value = mock_bm
mock_bm.baremetal = mock_bm
mock_bm.baremetal_introspection = mock_bm
introspector_client = mock_bm.baremetal_introspection
introspector_client.get_introspection_data = mock_bm
introspector_client.get_introspection_data.return_value = {
'inventory': {'disks': self.disks}
}
mock_bm.baremetal.nodes.side_effect = [
iter([self.fake_baremetal_node]),
iter([self.fake_baremetal_node])]
argslist = ['--all-manageable', '--deploy-ramdisk', 'test_ramdisk',
'--deploy-kernel', 'test_kernel']
verifylist = [('deploy_kernel', 'test_kernel'),
@ -750,14 +811,35 @@ class TestConfigureNode(fakes.TestOvercloudNode):
parsed_args = self.check_parser(self.cmd, argslist, verifylist)
self.cmd.take_action(parsed_args)
def test_configure_instance_boot_option(self):
def test_configure_instance_boot_option(self, mock_conn,
mock_connect, mock_conf,
mock_bm):
mock_conn.return_value = mock_bm
mock_bm.baremetal = mock_bm
mock_bm.baremetal.nodes.side_effect = [
iter([self.fake_baremetal_node]),
iter([self.fake_baremetal_node])]
argslist = ['--all-manageable', '--instance-boot-option', 'netboot']
verifylist = [('instance_boot_option', 'netboot')]
parsed_args = self.check_parser(self.cmd, argslist, verifylist)
self.cmd.take_action(parsed_args)
def test_configure_root_device(self):
def test_configure_root_device(self, mock_conn,
mock_connect, mock_conf,
mock_bm):
mock_conn.return_value = mock_bm
mock_bm.baremetal = mock_bm
mock_bm.baremetal_introspection = mock_bm
introspector_client = mock_bm.baremetal_introspection
introspector_client.get_introspection_data = mock_bm
introspector_client.get_introspection_data.return_value = {
'inventory': {'disks': self.disks}
}
mock_bm.baremetal.nodes.side_effect = [
iter([self.fake_baremetal_node]),
iter([self.fake_baremetal_node])]
argslist = ['--all-manageable',
'--root-device', 'smallest',
'--root-device-minimum-size', '2',
@ -772,7 +854,23 @@ class TestConfigureNode(fakes.TestOvercloudNode):
@mock.patch('tripleoclient.workflows.baremetal.'
'_apply_root_device_strategy')
def test_configure_specified_node_with_all_arguments(
self, mock_root_device):
self, mock_root_device, mock_conn,
mock_connect, mock_conf,
mock_bm):
mock_conn.return_value = mock_bm
mock_bm.baremetal = mock_bm
mock_bm.baremetal_introspection = mock_bm
introspector_client = mock_bm.baremetal_introspection
introspector_client.get_introspection_data = mock_bm
introspector_client.get_introspection_data.return_value = {
'inventory': {'disks': self.disks}
}
mock_bm.baremetal.nodes.side_effect = [
iter([self.fake_baremetal_node]),
iter([self.fake_baremetal_node])]
argslist = ['node_id',
'--deploy-kernel', 'test_kernel',
'--deploy-ramdisk', 'test_ramdisk',

View File

@ -108,12 +108,6 @@ class TestBaremetalWorkflows(fakes.FakePlaybookExecution):
node_timeout=1200, max_retries=1, retry_timeout=120,
)
def test_configure_success(self):
baremetal.configure(self.app.client_manager, node_uuids=[])
def test_configure_manageable_nodes_success(self):
baremetal.configure_manageable_nodes(self.app.client_manager)
def test_run_instance_boot_option(self):
result = baremetal._configure_boot(
self.app.client_manager,

View File

@ -333,36 +333,31 @@ class ConfigureNode(command.Command):
action='store_true',
help=_('Whether to overwrite existing root device '
'hints when --root-device is used.'))
parser.add_argument("--verbosity",
type=int,
default=1,
help=_("Print debug output during execution"))
return parser
def take_action(self, parsed_args):
self.log.debug("take_action(%s)" % parsed_args)
conf = tb.TripleoConfigure(
kernel_name=parsed_args.deploy_kernel,
ramdisk_name=parsed_args.deploy_ramdisk,
instance_boot_option=parsed_args.instance_boot_option,
boot_mode=parsed_args.boot_mode,
root_device=parsed_args.root_device,
root_device_minimum_size=parsed_args.root_device_minimum_size,
overwrite_root_device_hints=(
parsed_args.overwrite_root_device_hints)
)
if parsed_args.node_uuids:
baremetal.configure(
self.app.client_manager,
node_uuids=parsed_args.node_uuids,
kernel_name=parsed_args.deploy_kernel,
ramdisk_name=parsed_args.deploy_ramdisk,
instance_boot_option=parsed_args.instance_boot_option,
boot_mode=parsed_args.boot_mode,
root_device=parsed_args.root_device,
root_device_minimum_size=parsed_args.root_device_minimum_size,
overwrite_root_device_hints=(
parsed_args.overwrite_root_device_hints)
)
conf.configure(
node_uuids=parsed_args.node_uuids)
else:
baremetal.configure_manageable_nodes(
self.app.client_manager,
kernel_name=parsed_args.deploy_kernel,
ramdisk_name=parsed_args.deploy_ramdisk,
instance_boot_option=parsed_args.instance_boot_option,
boot_mode=parsed_args.boot_mode,
root_device=parsed_args.root_device,
root_device_minimum_size=parsed_args.root_device_minimum_size,
overwrite_root_device_hints=(
parsed_args.overwrite_root_device_hints)
)
conf.configure_manageable_nodes()
class DiscoverNode(command.Command):

View File

@ -351,105 +351,6 @@ def _apply_root_device_strategy(clients, node_uuid, strategy,
{'node': node.uuid, 'dev': root_device, 'local_gb': new_size})
def configure(clients, node_uuids, kernel_name=None,
ramdisk_name=None, instance_boot_option=None,
boot_mode=None, root_device=None, root_device_minimum_size=4,
overwrite_root_device_hints=False):
"""Configure Node boot options.
:param node_uuids: List of instance UUID(s).
:type node_uuids: List
:param kernel_name: Kernel to use
:type kernel_name: String
:param ramdisk_name: RAMDISK to use
:type ramdisk_name: String
:param instance_boot_option: Boot options to use
:type instance_boot_option: String
:param boot_mode: Boot mode to use
:type instance_boot_option: String
:param root_device: Path (name) of the root device.
:type root_device: String
:param root_device_minimum_size: Size of the given root device.
:type root_device_minimum_size: Integer
:param overwrite_root_device_hints: Whether to overwrite existing root
device hints when `root_device` is
used.
:type overwrite_root_device_hints: Boolean
"""
for node_uuid in node_uuids:
_configure_boot(clients, node_uuid, kernel_name,
ramdisk_name, instance_boot_option, boot_mode)
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',
ramdisk_name='bm-deploy-ramdisk',
instance_boot_option=None, boot_mode=None,
root_device=None, root_device_minimum_size=4,
overwrite_root_device_hints=False):
"""Configure all manageable Nodes.
kernel_name=parsed_args.deploy_kernel,
ramdisk_name=parsed_args.deploy_ramdisk,
instance_boot_option=parsed_args.instance_boot_option,
root_device=parsed_args.root_device,
root_device_minimum_size=parsed_args.root_device_minimum_size,
overwrite_root_device_hints=(parsed_args.overwrite_root_device_hints)
:param kernel_name: Kernel to use
:type kernel_name: String
:param ramdisk_name: RAMDISK to use
:type ramdisk_name: String
:param instance_boot_option: Boot options to use
:type instance_boot_option: String
:param boot_mode: Boot mode to use
:type instance_boot_option: String
:param root_device: Path (name) of the root device.
:type root_device: String
:param root_device_minimum_size: Size of the given root device.
:type root_device_minimum_size: Integer
:param overwrite_root_device_hints: Whether to overwrite existing root
device hints when `root_device` is
used.
:type overwrite_root_device_hints: Boolean
"""
configure(
clients=clients,
node_uuids=[
i.uuid for i in clients.baremetal.node.list()
if i.provision_state == "manageable" and not i.maintenance
],
kernel_name=kernel_name,
ramdisk_name=ramdisk_name,
instance_boot_option=instance_boot_option,
boot_mode=boot_mode,
root_device=root_device,
root_device_minimum_size=root_device_minimum_size,
overwrite_root_device_hints=overwrite_root_device_hints
)
def create_raid_configuration(clients, node_uuids, configuration,
verbosity=0):
"""Create RAID configuration on nodes.

View File

@ -19,7 +19,9 @@ from concurrent import futures
from openstack import connect as sdkclient
from openstack import exceptions
from openstack.utils import iterate_timeout
from oslo_utils import units
from tripleoclient import exceptions as ooo_exceptions
from tripleo_common.utils import nodes as node_utils
class TripleoBaremetal(object):
@ -309,3 +311,216 @@ class TripleoClean(TripleoBaremetal):
self.log.info(msg)
except exceptions.OpenStackCloudException as err:
self.log.error(str(err))
class TripleoConfigure(TripleoBaremetal):
"""TripleoConfigure handles properties for the ironic nodes.
We use this class to set the properties for each node such as the
kernel, ramdisk, boot device, root_device.
:param kernel_name: The name of the kernel image we will deploy
:type kernel_name: String
:param ramdisk_name: The name of the ramdisk image we will deploy
:type ramdisk_name: String
:param instance_boot: Should the node boot from local disks or something
else
:type instance_boot: String
:param boot_mode: Is this node using BIOS or UEFI
:type boot_mode: String
:param: root_device: What is the root device for this node. eg /dev/sda
:type root_device: String
:param root_device_minimum_size: What is the smallest disk we should
consider acceptable for deployment
:type root_device: Integer
:param overwrite_root_device_hints: Should we overwrite existing root
device hints when root_device is used.
:type overwrite_root_device_hints: Boolean
"""
log = logging.getLogger(__name__)
def __init__(self, kernel_name: str = None, ramdisk_name: str = None,
instance_boot_option: str = None, boot_mode: str = None,
root_device: str = None, verbosity: int = 0,
root_device_minimum_size: int = 4,
overwrite_root_device_hints: bool = False):
super().__init__(verbosity=verbosity)
self.kernel_name = kernel_name
self.ramdisk_name = ramdisk_name
self.instance_boot_option = instance_boot_option
self.boot_mode = boot_mode
self.root_device = root_device
self.root_device_minimum_size = root_device_minimum_size
self.overwrite_root_device_hints = overwrite_root_device_hints
def _apply_root_device_strategy(self, node_uuid: List,
strategy: str, minimum_size: int = 4,
overwrite: bool = False):
clients = self.conn
node = clients.baremetal.find_node(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.
self.log.warning('Root device hints are already set for node '
'{} and overwriting is not requested,'
' skipping'.format(node.id))
self.log.warning('You may unset them by running $ ironic '
'node-update {} remove '
'properties/root_device'.format(node.id))
return
inspector_client = self.conn.baremetal_introspection
baremetal_client = self.conn.baremetal
try:
data = inspector_client.get_introspection_data(node.id)
except Exception:
raise exceptions.RootDeviceDetectionError(
f'No introspection data found for node {node.id}, '
'root device cannot be detected')
try:
disks = data['inventory']['disks']
except KeyError:
raise exceptions.RootDeviceDetectionError(
f'Malformed introspection data for node {node.id}: '
'disks list is missing')
minimum_size *= units.Gi
disks = [d for d in disks if d.get('size', 0) >= minimum_size]
if not disks:
raise exceptions.RootDeviceDetectionError(
f'No suitable disks found for node {node.id}')
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(
f'Cannot find a disk with any of names {strategy} '
f'for node {node.id}')
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(
f"Neither WWN nor serial number are known for device "
f"{root_device['name']} "
f"on node {node.id}; root device hints cannot be used")
# 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.update_node(
node.id,
[{'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 %s, new device '
'is %s, new local_gb is %s',
node.id, root_device, new_size
)
def _configure_boot(self, node_uuid: List,
kernel_name: str = None,
ramdisk_name: str = None,
instance_boot_option: str = None,
boot_mode: str = None):
baremetal_client = self.conn.baremetal
image_ids = {'kernel': kernel_name, 'ramdisk': ramdisk_name}
node = baremetal_client.find_node(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
if boot_mode is not None:
capabilities['boot_mode'] = boot_mode
capabilities = node_utils.dict_to_capabilities(capabilities)
baremetal_client.update_node(node.id, [
{
'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 configure(self, node_uuids: List):
"""Configure Node boot options.
:param node_uuids: List of instance UUID(s).
:type node_uuids: List
"""
for node_uuid in node_uuids:
self._configure_boot(node_uuid, self.kernel_name,
self.ramdisk_name, self.instance_boot_option,
self.boot_mode)
if self.root_device:
self._apply_root_device_strategy(
node_uuid,
strategy=self.root_device,
minimum_size=self.root_device_minimum_size,
overwrite=self.overwrite_root_device_hints)
self.log.info('Successfully configured the nodes.')
def configure_manageable_nodes(self):
self.configure(node_uuids=self.all_manageable_nodes())