Add test for dhcp-less vmedia based deployment

Creating test to help facilitate the fix of bug 2032377 and
ultimately help ensure we have a backwards compatible fix.

The scenario:

* Creates a shiny new network without dhcp, and adds a router.
* Sets that network as the provisioning network for the node.
* Creates a port on that network, assigns that port as the VIF.
* Creates a configuration drive network_data.json file payload
* Triggers deployment, utilizing the aformentioned network_data
  payload and configuration drive.
* Once deployment has complete, attempts to ping the remote host.
* Once pingable, and this is configurable, it will then attempt
  to rebuild the remote host, basically forcing the condition
  covered in bug #2032377.
* Rebuild is completed, and the host is attempted to be pinged,
  again.

To facilitate this, three configuration options have been added.
Two are external network settings. Because there is no way in
a devstack configuration of neutron to advertise the next hop
router, we have to have a pre-assigned/configured IP on the
external network we can attach a router to. We also need to know
the external network ID.

Then there is basically a option flag if we wish to exercise the
pattern for bug 2032377. Ideally, this would be always, but with
the complexity and fact a non-stock IPA image is required, one
sort of needs to know and then configure as appropriate.

Change-Id: Ic848b8051e4d863f30d47c833d334afc879e4f20
This commit is contained in:
Julia Kreger 2023-10-11 12:40:31 -07:00
parent 3c8235ed02
commit 793608a857
3 changed files with 286 additions and 13 deletions

View File

@ -238,6 +238,17 @@ BaremetalGroup = [
cfg.StrOpt('default_boot_option',
# No good default here, we need to actually set it.
help="The default boot option the testing nodes are using."),
cfg.BoolOpt("rebuild_remote_dhcpless",
default=True,
help="If we should issue a rebuild request when testing "
"dhcpless virtual media deployments. This may be useful "
"if bug 2032377 is not fixed in the agent ramdisk."),
cfg.StrOpt("public_subnet_id",
help="The public subnet ID where routers will be bound for "
"testing purposes with the dhcp-less test scenario."),
cfg.StrOpt("public_subnet_ip",
help="The public subnet IP to bind the public router to for "
"dhcp-less testing.")
]
BaremetalFeaturesGroup = [
@ -258,6 +269,14 @@ BaremetalFeaturesGroup = [
default=False,
help="Defines if in-band RAID can be built in deploy time "
"(possible starting with Victoria)."),
cfg.BoolOpt('dhcpless_vmedia',
default=False,
help="Defines if it is possible to execute DHCP-Less "
"deployment of baremetal nodes through virtual media. "
"This test requires full OS images with configuration "
"support for embedded network metadata through glean "
"or cloud-init, and thus cannot be executed with "
"most default job configurations."),
]
BaremetalIntrospectionGroup = [

View File

@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import ipaddress
import random
from oslo_utils import uuidutils
@ -264,9 +265,67 @@ class BaremetalStandaloneManager(bm.BaremetalScenarioTest,
'ironic node uuid %s' % node['instance_uuid'])
raise lib_exc.TimeoutException(msg)
@classmethod
def gen_config_drive_net_info(cls, node_id, n_port):
# Find the port with the vif.
use_port = None
_, body = cls.baremetal_client.list_node_ports(node_id)
for port in body['ports']:
_, p = cls.baremetal_client.show_port(port['uuid'])
if 'tenant_vif_port_id' in p['internal_info']:
use_port = p
break
if not use_port:
m = ('Unable to determine proper mac address to use for config '
'to apply for the virtual media port test.')
raise lib_exc.InvalidConfiguration(m)
vif_mac_address = use_port['address']
if CONF.validation.ip_version_for_ssh == 4:
ip_version = "ipv4"
else:
ip_version = "ipv6"
ip_address = n_port['fixed_ips'][0]['ip_address']
subnet_id = n_port['fixed_ips'][0]['subnet_id']
subnet = cls.os_primary.subnets_client.show_subnet(
subnet_id).get('subnet')
ip_netmask = str(ipaddress.ip_network(subnet.get('cidr')).netmask)
if ip_version == "ipv4":
route = [{
"netmask": "0.0.0.0",
"network": "0.0.0.0",
"gateway": subnet.get('gateway_ip'),
}]
else:
# Eh... the data structure doesn't really allow for
# this to be easy since a default route with v6
# is just referred to as ::/0
# so network and netmask would be ::, which is
# semi-mind-breaking. Anyway, route advertisers are
# expected in this case.
route = []
return {
"links": [{"id": "port-test",
"type": "vif",
"ethernet_mac_address": vif_mac_address}],
"networks": [
{
"id": "network0",
"type": ip_version,
"link": "port-test",
"ip_address": ip_address,
"netmask": ip_netmask,
"network_id": "network0",
"routes": route
}
],
"services": []
}
@classmethod
def boot_node(cls, image_ref=None, image_checksum=None,
boot_option=None):
boot_option=None, config_drive_networking=False,
fallback_network=None):
"""Boot ironic node.
The following actions are executed:
@ -282,7 +341,13 @@ class BaremetalStandaloneManager(bm.BaremetalScenarioTest,
:param boot_option: The defaut boot option to utilize. If not
specified, the ironic deployment default shall
be utilized.
:param config_drive_networking: If we should load configuration drive
with network_data values.
:param fallback_network: Network to use if we are not able to detect
a network for use.
"""
config_drive = {}
if image_ref is None:
image_ref = cls.image_ref
if image_checksum is None:
@ -291,8 +356,22 @@ class BaremetalStandaloneManager(bm.BaremetalScenarioTest,
boot_option = cls.boot_option
network, subnet, router = cls.create_networks()
n_port = cls.create_neutron_port(network_id=network['id'])
try:
n_port = cls.create_neutron_port(network_id=network['id'])
except TypeError:
if fallback_network:
n_port = cls.create_neutron_port(
network_id=fallback_network)
else:
raise
cls.vif_attach(node_id=cls.node['uuid'], vif_id=n_port['id'])
config_drive = None
if config_drive_networking:
config_drive = {}
config_drive['network_data'] = cls.gen_config_drive_net_info(
cls.node['uuid'], n_port)
patch = [{'path': '/instance_info/image_source',
'op': 'add',
'value': image_ref}]
@ -308,9 +387,15 @@ class BaremetalStandaloneManager(bm.BaremetalScenarioTest,
patch.append({'path': '/instance_info/capabilities',
'op': 'add',
'value': {'boot_option': boot_option}})
# TODO(vsaienko) add testing for custom configdrive
cls.update_node(cls.node['uuid'], patch=patch)
cls.set_node_provision_state(cls.node['uuid'], 'active')
if not config_drive:
cls.set_node_provision_state(cls.node['uuid'], 'active')
else:
cls.set_node_provision_state(
cls.node['uuid'], 'active',
configdrive=config_drive)
cls.wait_power_state(cls.node['uuid'],
bm.BaremetalPowerStates.POWER_ON)
cls.wait_provisioning_state(cls.node['uuid'],
@ -332,7 +417,12 @@ class BaremetalStandaloneManager(bm.BaremetalScenarioTest,
cls.detach_all_vifs_from_node(node_id, force_delete=force_delete)
if cls.delete_node or force_delete:
cls.set_node_provision_state(node_id, 'deleted')
node_state = cls.get_node(node_id)['provision_state']
if node_state != bm.BaremetalProvisionStates.AVAILABLE:
# Check the state before making the call, to permit tests to
# drive node into a clean state before exiting the test, which
# is needed for some tests because of complex tests.
cls.set_node_provision_state(node_id, 'deleted')
# NOTE(vsaienko) We expect here fast switching from deleted to
# available as automated cleaning is disabled so poll status
# each 1s.
@ -579,9 +669,16 @@ class BaremetalStandaloneScenarioTest(BaremetalStandaloneManager):
'Partitioned images are not supported with multitenancy.')
@classmethod
def set_node_to_active(cls, image_ref=None, image_checksum=None):
cls.boot_node(image_ref, image_checksum)
if CONF.validation.connect_method == 'floating':
def set_node_to_active(cls, image_ref=None, image_checksum=None,
fallback_network=None,
config_drive_networking=None,
method_to_get_ip=None):
cls.boot_node(image_ref, image_checksum,
fallback_network=fallback_network,
config_drive_networking=config_drive_networking)
if method_to_get_ip:
cls.node_ip = method_to_get_ip(cls.node['uuid'])
elif 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'])
@ -622,11 +719,7 @@ class BaremetalStandaloneScenarioTest(BaremetalStandaloneManager):
cls.update_node_driver(cls.node['uuid'], cls.driver, **boot_kwargs)
@classmethod
def resource_cleanup(cls):
if CONF.validation.connect_method == 'floating':
if cls.node_ip:
cls.cleanup_floating_ip(cls.node_ip)
def cleanup_vif_attachments(cls):
vifs = cls.get_node_vifs(cls.node['uuid'])
# Remove ports before deleting node, to catch regression for cases
# when user did this prior unprovision node.
@ -635,6 +728,18 @@ class BaremetalStandaloneScenarioTest(BaremetalStandaloneManager):
cls.ports_client.delete_port(vif)
except lib_exc.NotFound:
pass
@classmethod
def resource_cleanup(cls):
if CONF.validation.connect_method == 'floating':
if cls.node_ip:
try:
cls.cleanup_floating_ip(cls.node_ip)
except IndexError:
# There is no fip to actually remove in this case.
pass
cls.cleanup_vif_attachments()
cls.terminate_node(cls.node['uuid'])
cls.unreserve_node(cls.node)
base.reset_baremetal_api_microversion()

View File

@ -0,0 +1,149 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from tempest.common import utils
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
from ironic_tempest_plugin.tests.scenario import \
baremetal_standalone_manager as bsm
CONF = config.CONF
class BaremetalRedfishDHCPLessDeploy(bsm.BaremetalStandaloneScenarioTest):
api_microversion = '1.59' # Ussuri for redfish-virtual-media
driver = 'redfish'
deploy_interface = 'direct'
boot_interface = 'redfish-virtual-media'
image_ref = CONF.baremetal.whole_disk_image_ref
image_checksum = CONF.baremetal.whole_disk_image_checksum
wholedisk_image = True
@classmethod
def skip_checks(cls):
super(BaremetalRedfishDHCPLessDeploy, cls).skip_checks()
if CONF.baremetal_feature_enabled.dhcpless_vmedia:
raise cls.skipException("This test requires a full OS image to "
"be deployed, and thus must be "
"explicitly enabled for testing.")
if (not CONF.baremetal.public_subnet_id
or not CONF.baremetal.public_subnet_ip):
raise cls.skipException(
"This test requires a public sunbet ID, and public subnet "
"IP to use on that subnet to execute. Please see the "
"baremetal configuration options public_subnet_id "
"and public_subnet_ip respectively, and populate with "
"appropriate values to execute this test.")
def create_tenant_network(self, clients, tenant_cidr, ip_version):
# NOTE(TheJulia): self.create_network is an internal method
# which just gets the info, doesn't actually create a network.
network = self.create_network(
networks_client=self.os_admin.networks_client,
project_id=clients.credentials.project_id,
shared=True)
router = self.get_router(
client=clients.routers_client,
project_id=clients.credentials.tenant_id,
external_gateway_info={
'network_id': CONF.network.public_network_id,
'external_fixed_ips': [
{'subnet_id': CONF.baremetal.public_subnet_id,
'ip_address': CONF.baremetal.public_subnet_ip}]
})
result = clients.subnets_client.create_subnet(
name=data_utils.rand_name('subnet'),
network_id=network['id'],
tenant_id=clients.credentials.tenant_id,
ip_version=CONF.validation.ip_version_for_ssh,
cidr=tenant_cidr, enable_dhcp=False)
subnet = result['subnet']
clients.routers_client.add_router_interface(router['id'],
subnet_id=subnet['id'])
self.addCleanup(clients.subnets_client.delete_subnet, subnet['id'])
self.addCleanup(clients.routers_client.remove_router_interface,
router['id'], subnet_id=subnet['id'])
return network, subnet, router
def deploy_vmedia_dhcpless(self, rebuild=False):
"""Helper to facilitate vmedia testing.
* Create Network/router without DHCP
* Set provisioning_network for this node.
* Set cleanup to undo the provisionign network setup.
* Launch instance.
* Requirement: Instance OS image supports network config from
network_data embedded in the OS. i.e. a real image, not
cirros.
* If so enabled, rebuild the node, Verify rebuild completed.
* Via cleanup: Teardown Network/Router
"""
# Get the latest state for the node.
self.node = self.get_node(self.node['uuid'])
prior_prov_net = self.node['driver_info'].get('provisioning_network')
ip_version = CONF.validation.ip_version_for_ssh
tenant_cidr = '10.0.6.0/24'
if ip_version == 6:
tenant_cidr = 'fd00:33::/64'
network, subnet, router = self.create_tenant_network(
self.os_admin, tenant_cidr, ip_version=ip_version)
if prior_prov_net:
self.update_node(self.node['uuid'],
[{'op': 'replace',
'path': '/driver_info/provisioning_network',
'value': network['id']}])
self.addCleanup(self.update_node,
self.node['uuid'],
[{'op': 'replace',
'path': '/driver_info/provisioning_network',
'value': prior_prov_net}])
else:
self.update_node(self.node['uuid'],
[{'op': 'add',
'path': '/driver_info/provisioning_network',
'value': network['id']}])
self.addCleanup(self.update_node,
self.node['uuid'],
[{'op': 'remove',
'path': '/driver_info/provisioning_network'}])
self.set_node_to_active(self.image_ref, self.image_checksum,
fallback_network=network['id'],
config_drive_networking=True,
method_to_get_ip=self.get_server_ip)
# node_ip is set by the prior call to set_node_to_active
self.assertTrue(self.ping_ip_address(self.node_ip))
if rebuild:
self.set_node_provision_state(self.node['uuid'], 'rebuild')
self.wait_provisioning_state(self.node['uuid'], 'active',
timeout=CONF.baremetal.active_timeout,
interval=30)
# Assert we were able to ping after rebuilding.
self.assertTrue(self.ping_ip_address(self.node_ip))
# Force delete so we remove the vifs
self.terminate_node(self.node['uuid'], force_delete=True)
@decorators.idempotent_id('1f420ef3-99bd-46c7-b859-ce9c2892697f')
@utils.services('image', 'network')
def test_ip_access_to_server(self):
self.deploy_vmedia_dhcpless(
rebuild=CONF.baremetal.rebuild_remote_dhcpless)