Tempest test for anaconda deploy

Provides a test and substrate changes to support integration
testing of the anaconda deployment interface from a "standalone"
perspect.

This is present in two forms, a "with stage2 ramdisk" and
"without stage2" test which is enabled, or not depening
on the underlying configuration.

This test also has two modes of operation, the first and
default being primarily a "did anaconda start and can I
ping the machine?" test mode. The second attempts to wait
for the node to reach an active state, although it is not
the default because an anaconda deployment, depending on
mode of use, even with a default configuration can take
a substantial amount of itme. The anaconda deployment
interface is also modeled for highly tuned configurations,
so the prime aspect is "does it boot? does anaconda start?"

Also:
* Removes the explicit requirement that test classes explicitly
  declare support for wholedisk_image or not.

Change-Id: I42933d26268b55737fa2508265643c1cd14651ea
This commit is contained in:
Julia Kreger 2022-03-30 16:40:44 -07:00
parent a2c26c6ddf
commit 982d177007
8 changed files with 207 additions and 4 deletions

View File

@ -74,7 +74,8 @@ def wait_for_bm_node_status(client, node_id, attr, status, timeout=None,
if node[attr] in status:
return True
elif (abort_on_error_state
and node['provision_state'].endswith(' failed')):
and (node['provision_state'].endswith(' failed')
or node['provision_state'] == 'error')):
msg = ('Node %(node)s reached failure state %(state)s while '
'waiting for %(attr)s=%(expected)s. '
'Error: %(error)s' %
@ -159,3 +160,35 @@ def wait_for_allocation(client, allocation_ident, timeout=15, interval=1,
raise lib_exc.TimeoutException(msg)
return result[0]
def wait_node_value_in_field(client, node_id, field, value,
raise_if_insufficent_access=True,
timeout=None, interval=None):
"""Waits for a node to have a field value appear.
:param client: an instance of tempest plugin BaremetalClient.
:param node_id: the UUID of the node
:param field: the field in the node object to examine
:param value: the value/key with-in the field to look for.
:param timeout: the timeout after which the check is considered as failed.
:param interval: an interval between show_node calls for status check.
"""
def is_field_updated():
node = utils.get_node(client, node_id=node_id)
field_value = node[field]
if raise_if_insufficent_access and '** Redacted' in field_value:
msg = ('Unable to see contents of redacted field '
'indicating insufficent access to execute this test.')
raise lib_exc.InsufficientAPIAccess(msg)
return value in field_value
if not test_utils.call_until_true(is_field_updated, timeout,
interval):
msg = ('Timed out waiting to get Ironic node by node_id '
'%(node_id)s within the required time (%(timeout)s s). '
'Field value %(value) did not appear in field %(field)s.'
% {'node_id': node_id, 'timeout': timeout,
'field': field, 'value': value})
raise lib_exc.TimeoutException(msg)

View File

@ -89,6 +89,10 @@ BaremetalGroup = [
cfg.IntOpt('active_timeout',
default=300,
help="Timeout for Ironic node to completely provision"),
cfg.IntOpt('anaconda_active_timeout',
default=3600,
help="Timeout for Ironic node to completely provision "
"when using the anaconda deployment interface."),
cfg.IntOpt('association_timeout',
default=30,
help="Timeout for association of Nova instance and Ironic "
@ -146,6 +150,28 @@ BaremetalGroup = [
cfg.StrOpt('rollback_import_location',
help="Rollback import config location for configuration "
"molds. Optional. If not provided, rollback is skipped."),
# TODO(TheJulia): For now, anaconda can be url based and we can move in
# to being tested with glance as soon as we get a public stage2 image.
cfg.StrOpt('anaconda_image_ref',
help="URL of an anaconda repository to set as image_source"),
cfg.StrOpt('anaconda_kernel_ref',
help="URL of the kernel to utilize for anaconda deploys."),
cfg.StrOpt('anaconda_initial_ramdisk_ref',
help="URL of the initial ramdisk to utilize for anaconda "
"deploy operations."),
cfg.StrOpt('anaconda_stage2_ramdisk_ref',
help="URL of the anaconda second stage ramdisk. Presence of "
"this setting will also determine if a stage2 specific "
"anaconda test is run, or not."),
cfg.StrOpt('anaconda_exit_test_at',
default='heartbeat',
choices=['heartbeat', 'active'],
help='When to end the anaconda test job at. Due to '
'the use model of the anaconda driver, as well '
'as the performance profile, the anaconda test is '
'normally only executed until we observe a heartbeat '
'operation indicating that anaconda *has* booted and '
'successfully parsed the URL.'),
cfg.ListOpt('enabled_drivers',
default=['fake', 'pxe_ipmitool', 'agent_ipmitool'],
help="List of Ironic enabled drivers."),

View File

@ -27,3 +27,8 @@ class HypervisorUpdateTimeout(exceptions.TempestException):
class RaidCleaningInventoryValidationFailed(exceptions.TempestException):
message = "RAID cleaning storage inventory validation failed"
class InsufficientAPIAccess(exceptions.TempestException):
message = ("Insufficent Access to the API exists. Please use a user "
"with an elevated level of access to execute this test.")

View File

@ -123,6 +123,16 @@ class BaremetalScenarioTest(manager.ScenarioTest):
ironic_waiters.wait_node_instance_association(self.baremetal_client,
instance_id)
@classmethod
def wait_for_agent_heartbeat(cls, node_id, timeout=None):
ironic_waiters.wait_node_value_in_field(
cls.baremetal_client,
node_id=node_id,
field='driver_internal_info',
value='agent_last_heartbeat',
timeout=timeout or CONF.baremetal.deploywait_timeout,
interval=10)
@classmethod
def get_node(cls, node_id=None, instance_id=None, api_version=None):
return utils.get_node(cls.baremetal_client, node_id, instance_id,

View File

@ -503,7 +503,7 @@ class BaremetalStandaloneScenarioTest(BaremetalStandaloneManager):
# If we need to set provision state 'deleted' for the node after test
delete_node = True
mandatory_attr = ['driver', 'image_ref', 'wholedisk_image']
mandatory_attr = ['driver', 'image_ref']
node = None
node_ip = None
@ -570,7 +570,11 @@ class BaremetalStandaloneScenarioTest(BaremetalStandaloneManager):
"in the list of enabled power interfaces %(enabled)s" % {
'iface': cls.power_interface,
'enabled': CONF.baremetal.enabled_power_interfaces})
if not cls.wholedisk_image and CONF.baremetal.use_provision_network:
if (cls.wholedisk_image is not None
and not cls.wholedisk_image
and CONF.baremetal.use_provision_network):
# We only want to enter here if cls.wholedisk_image is set to
# a value. If None, skip, if True/False go from there.
raise cls.skipException(
'Partitioned images are not supported with multitenancy.')
@ -796,3 +800,79 @@ class BaremetalStandaloneScenarioTest(BaremetalStandaloneManager):
self.boot_node_ramdisk(ramdisk_ref, iso)
self.assertTrue(self.ping_ip_address(self.node_ip,
should_succeed=should_succeed))
@classmethod
def boot_node_anaconda(cls, image_ref, kernel_ref, ramdisk_ref,
stage2_ref=None):
"""Boot ironic using a ramdisk node.
The following actions are executed:
* Create/Pick networks to boot node in.
* Create Neutron port and attach it to node.
* Update node image_source.
* Deploy node.
* Wait until node is deployed.
:param ramdisk_ref: Reference to user image or ramdisk to boot
the node with.
:param iso: Boolean, default False, to indicate if the image ref
us actually an ISO image.
"""
if image_ref is None or kernel_ref is None or ramdisk_ref is None:
raise cls.skipException('Skipping anaconda tests as an image ref '
'was not supplied')
network, subnet, router = cls.create_networks()
n_port = cls.create_neutron_port(network_id=network['id'])
cls.vif_attach(node_id=cls.node['uuid'], vif_id=n_port['id'])
p_root = '/instance_info/'
patch = [{'path': p_root + 'image_source',
'op': 'add',
'value': image_ref},
{'path': p_root + 'kernel',
'op': 'add',
'value': kernel_ref},
{'path': p_root + 'ramdisk',
'op': 'add',
'value': ramdisk_ref}]
if stage2_ref:
patch.append(
{
'path': p_root + 'stage2',
'op': 'add',
'value': stage2_ref,
}
)
cls.update_node(cls.node['uuid'], patch=patch)
cls.set_node_provision_state(cls.node['uuid'], 'active')
if CONF.validation.connect_method == 'floating':
cls.node_ip = cls.add_floatingip_to_node(cls.node['uuid'])
elif CONF.validation.connect_method == 'fixed':
cls.node_ip = cls.get_server_ip(cls.node['uuid'])
else:
m = ('Configuration option "[validation]/connect_method" '
'must be set.')
raise lib_exc.InvalidConfiguration(m)
cls.wait_power_state(cls.node['uuid'],
bm.BaremetalPowerStates.POWER_ON)
if CONF.baremetal.anaconda_exit_test_at == 'heartbeat':
cls.wait_for_agent_heartbeat(
cls.node['uuid'],
timeout=CONF.baremetal.anaconda_active_timeout)
elif CONF.baremetal.anaconda_exit_test_at == 'active':
cls.wait_provisioning_state(
cls.node['uuid'],
bm.BaremetalProvisionStates.ACTIVE,
timeout=CONF.baremetal.anaconda_active_timeout,
interval=30)
def boot_and_verify_anaconda_node(self,
image_ref=None,
kernel_ref=None,
ramdisk_ref=None,
stage2_ref=None,
should_succeed=True):
self.boot_node_anaconda(image_ref, kernel_ref, ramdisk_ref)
self.assertTrue(self.ping_ip_address(self.node_ip,
should_succeed=should_succeed))

View File

@ -654,3 +654,49 @@ class BaremetalIdracSyncBootModeDirectWholedisk(
self.wait_provisioning_state(self.node['uuid'], 'active',
timeout=CONF.baremetal.active_timeout,
interval=30)
class BaremetalRedfishIPxeAnacondaNoGlance(
bsm.BaremetalStandaloneScenarioTest):
api_microversion = '1.78' # to set the deploy_interface
driver = 'redfish'
deploy_interface = 'anaconda'
boot_interface = 'ipxe'
image_ref = CONF.baremetal.anaconda_image_ref
wholedisk_image = None
@classmethod
def skip_checks(cls):
super(BaremetalRedfishIPxeAnacondaNoGlance, cls).skip_checks()
if 'anaconda' not in CONF.baremetal.enabled_deploy_interfaces:
skip_msg = ("Skipping the test case since anaconda is not "
"enabled.")
raise cls.skipException(skip_msg)
def test_ip_access_to_server_without_stage2(self):
# Tests deploy from a URL, or a pre-existing anaconda reference in
# glance.
if CONF.baremetal.anaconda_stage2_ramdisk_ref is not None:
skip_msg = ("Skipping the test case as an anaconda stage2 "
"ramdisk has been defined, and that test can "
"run instead.")
raise self.skipException(skip_msg)
self.boot_and_verify_anaconda_node(
image_ref=self.image_ref,
kernel_ref=CONF.baremetal.anaconda_kernel_ref,
ramdisk_ref=CONF.baremetal.anaconda_initial_ramdisk_ref)
def test_ip_access_to_server_using_stage2(self):
# Tests anaconda with a second stage ramdisk
if CONF.baremetal.anaconda_stage2_ramdisk_ref is None:
skip_msg = ("Skipping as stage2 ramdisk ref pointer has "
"not been configured.")
raise self.skipException(skip_msg)
self.boot_and_verify_anaconda_node(
image_ref=self.image_ref,
kernel_ref=CONF.baremetal.anaconda_kernel_ref,
ramdisk_ref=CONF.baremetal.anaconda_initial_ramdisk_ref,
stage2_ref=CONF.baremetal.anaconda_stage2_ramdisk_ref)

View File

@ -189,7 +189,9 @@ class BaremetalBasicOps(baremetal_manager.BaremetalScenarioTest):
def validate_image(self):
iinfo = self.node['instance_info']
if self.wholedisk_image:
if self.wholedisk_image is not None and self.wholedisk_image:
# If None, we have nothing to do here. If False, we don't
# want to fall into this either.
self.assertNotIn('kernel', iinfo)
self.assertNotIn('ramdisk', iinfo)
else:

View File

@ -21,6 +21,7 @@
- ironic-inspector-tempest-xena
- ironic-inspector-tempest-wallaby:
voting: false
- ironic-standalone-anaconda
- ironic-standalone-redfish
- ironic-standalone-redfish-yoga:
voting: false