Files
zaza-openstack-tests/zaza/openstack/charm_tests/nova/tests.py
Samuel Walladge c396608daa Fix pep8 lint errors
One of the pep8 target dependencies must have updated,
causing a bunch of new lint errors in these categories:
- line length > 79 chars
- no whitespace after keyword
2022-08-03 13:44:22 +09:30

1168 lines
48 KiB
Python

#!/usr/bin/env python3
# Copyright 2018 Canonical Ltd.
#
# 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.
"""Encapsulate nova testing."""
import json
import logging
import os
import tempfile
import unittest
import urllib
from configparser import ConfigParser
from time import sleep
import novaclient.exceptions
import zaza.model
import zaza.openstack.charm_tests.glance.setup as glance_setup
import zaza.openstack.charm_tests.neutron.tests as neutron_tests
import zaza.openstack.charm_tests.nova.utils as nova_utils
import zaza.openstack.charm_tests.test_utils as test_utils
import zaza.openstack.utilities.generic as generic_utils
import zaza.openstack.configure.guest as guest
import zaza.openstack.utilities.openstack as openstack_utils
from zaza.utilities import juju as juju_utils
class BaseGuestCreateTest(unittest.TestCase):
"""Deprecated: Use zaza.openstack.configure.guest.launch_instance."""
def launch_instance(self, instance_key):
"""Deprecated: Use zaza.openstack.configure.guest.launch_instance."""
logging.info('BaseGuestCreateTest.launch_instance is deprecated '
'please use '
'zaza.openstack.configure.guest.launch_instance')
guest.launch_instance(instance_key)
class CirrosGuestCreateTest(test_utils.OpenStackBaseTest):
"""Tests to launch a cirros image."""
def test_launch_small_instance(self):
"""Launch a cirros instance and test connectivity."""
self.RESOURCE_PREFIX = 'zaza-nova'
self.launch_guest(
'cirros', instance_key=glance_setup.CIRROS_IMAGE_NAME)
def tearDown(self):
"""Cleanup of VM guests."""
self.resource_cleanup()
class LTSGuestCreateTest(test_utils.OpenStackBaseTest):
"""Tests to launch a LTS image."""
def test_launch_small_instance(self):
"""Launch a Bionic instance and test connectivity."""
self.RESOURCE_PREFIX = 'zaza-nova'
self.launch_guest(
'ubuntu', instance_key=glance_setup.LTS_IMAGE_NAME)
def tearDown(self):
"""Cleanup of VM guests."""
self.resource_cleanup()
class LTSGuestCreateVolumeBackedTest(test_utils.OpenStackBaseTest):
"""Tests to launch a LTS image."""
def test_launch_small_instance(self):
"""Launch a Bionic instance and test connectivity."""
self.RESOURCE_PREFIX = 'zaza-nova'
self.launch_guest(
'volume-backed-ubuntu', instance_key=glance_setup.LTS_IMAGE_NAME,
use_boot_volume=True)
def tearDown(self):
"""Cleanup of VM guests."""
self.resource_cleanup()
class VTPMGuestCreateTest(test_utils.OpenStackBaseTest):
"""Tests launching a guest with vTPM Support.
These tests are only run for focal-wallaby and newer.
Base version in Wallaby is 23.0.0.
"""
def _check_tpm_device(self, instance, *devices):
"""Check that the instance has TPM devices available.
:param instance: the instance to determine if TPM devices are available
:type instance: nova_client.Server instance
:param devices: the devices to look for that are present in the guest
:type devices: list of strings
:return: True if the instance has TPM devices, False otherwise
:rtype: bool
"""
fip = neutron_tests.floating_ips_from_instance(instance)[0]
username = guest.boot_tests['focal']['username']
password = guest.boot_tests['focal'].get('password')
privkey = openstack_utils.get_private_key(nova_utils.KEYPAIR_NAME)
def check_tpm(stdin, stdout, stderr):
devs = [line.strip() for line in stdout.readlines()]
for expected in devices:
self.assertIn(expected, devs)
logging.info('Validating TPM devices are present')
openstack_utils.ssh_command(username, ip=fip, vm_name=instance.name,
command='sudo ls -1 /dev/tpm*',
password=password, privkey=privkey,
verify=check_tpm)
@test_utils.skipUntilVersion('nova-compute', 'nova-common', '3:23.0.0')
def test_launch_vtpm_1_2_instance(self):
"""Launch an instance using TPM 1.2."""
self.RESOURCE_PREFIX = 'zaza-nova'
instance = guest.launch_instance(
'focal', image_name='focal', flavor_name='vtpm-1.2',
vm_name='zaza-nova-vtpm-1-2',
)
# Note: TPM 1.2 presents tpm0 as a device
self._check_tpm_device(instance, '/dev/tpm0')
@test_utils.skipUntilVersion('nova-compute', 'nova-common', '3:23.0.0')
def test_launch_vtpm_2_instance(self):
"""Launch an instance using TPM 2.0."""
self.RESOURCE_PREFIX = 'zaza-nova'
instance = guest.launch_instance(
'focal', image_name='focal', flavor_name='vtpm-2',
vm_name='zaza-nova-vtpm-2',
)
# Note: TPM 1.2 and 2.0 both present tpm0 as a device. TPM 2.0
# devices also include a tpmrm0 device.
self._check_tpm_device(instance, '/dev/tpm0', '/dev/tpmrm0')
def tearDown(self):
"""Cleanup of VM guests."""
self.resource_cleanup()
class NovaCommonTests(test_utils.OpenStackBaseTest):
"""nova-compute and nova-cloud-controller common tests."""
XENIAL_MITAKA = openstack_utils.get_os_release('xenial_mitaka')
XENIAL_OCATA = openstack_utils.get_os_release('xenial_ocata')
XENIAL_QUEENS = openstack_utils.get_os_release('xenial_queens')
BIONIC_QUEENS = openstack_utils.get_os_release('bionic_queens')
BIONIC_ROCKY = openstack_utils.get_os_release('bionic_rocky')
BIONIC_TRAIN = openstack_utils.get_os_release('bionic_train')
@classmethod
def setUpClass(cls):
"""Run class setup for running nova-cloud-controller tests."""
super(NovaCommonTests, cls).setUpClass()
cls.current_release = openstack_utils.get_os_release()
def _test_pci_alias_config(self, app_name, service_list):
logging.info('Checking pci aliases in nova config...')
# Expected default and alternate values
current_value = zaza.model.get_application_config(
app_name)['pci-alias']
try:
current_value = current_value['value']
except KeyError:
current_value = None
new_value = '[{}, {}]'.format(
json.dumps({
'name': 'IntelNIC',
'capability_type': 'pci',
'product_id': '1111',
'vendor_id': '8086',
'device_type': 'type-PF'
}, sort_keys=True),
json.dumps({
'name': ' Cirrus Logic ',
'capability_type': 'pci',
'product_id': '0ff2',
'vendor_id': '10de',
'device_type': 'type-PCI'
}, sort_keys=True))
set_default = {'pci-alias': current_value}
set_alternate = {'pci-alias': new_value}
expected_conf_section = 'pci'
expected_conf_key = 'alias'
default_entry = {expected_conf_section: {}}
alternate_entry = {expected_conf_section: {
expected_conf_key: [
('{"capability_type": "pci", "device_type": "type-PF", '
'"name": "IntelNIC", "product_id": "1111", '
'"vendor_id": "8086"}'),
('{"capability_type": "pci", "device_type": "type-PCI", '
'"name": " Cirrus Logic ", "product_id": "0ff2", '
'"vendor_id": "10de"}')]}}
# Config file affected by juju set config change
conf_file = '/etc/nova/nova.conf'
# Make config change, check for service restarts
logging.info(
'Setting config on {} to {}'.format(app_name, set_alternate))
self.restart_on_changed(
conf_file,
set_default,
set_alternate,
default_entry,
alternate_entry,
service_list)
class CloudActions(test_utils.OpenStackBaseTest):
"""Test actions from actions/cloud.py."""
def fetch_nova_service_hostname(self, unit_name):
"""
Fetch hostname used to register with nova-cloud-controller.
When nova-compute registers with nova-cloud-controller it uses either
config variable from '/etc/nova/nova.conf` or host's hostname to
identify itself. We need to fetch this value directly from the unit,
otherwise it's not possible to correlate entries from
`nova service-list` with nova-compute units.
:param unit_name: nova-compute unit name.
:return: hostname used when registering to cloud-controller
"""
nova_cfg = ConfigParser()
result = zaza.model.run_on_unit(unit_name,
'cat /etc/nova/nova.conf')
nova_cfg.read_string(result['Stdout'])
try:
nova_service_name = nova_cfg['DEFAULT']['host']
except KeyError:
# Fallback to hostname if 'host' variable is not present in the
# config
result = zaza.model.run_on_unit(unit_name, 'hostname')
nova_service_name = result['Stdout'].rstrip('\n')
if not nova_service_name:
self.fail("Failed to fetch nova service name from"
" nova-compute unit.")
return nova_service_name
def test_940_enable_disable_actions(self):
"""Test disable/enable actions on nova-compute units."""
nova_units = zaza.model.get_units('nova-compute',
model_name=self.model_name)
# Check that nova-compute services are enabled before testing
for service in self.nova_client.services.list(binary='nova-compute'):
self.assertEqual(service.status, 'enabled')
# Run 'disable' action on units
zaza.model.run_action_on_units([unit.name for unit in nova_units],
'disable')
# Check action results via nova API
for service in self.nova_client.services.list(binary='nova-compute'):
self.assertEqual(service.status, 'disabled')
# Run 'enable' action on units
zaza.model.run_action_on_units([unit.name for unit in nova_units],
'enable')
# Check action results via nova API
for service in self.nova_client.services.list(binary='nova-compute'):
self.assertEqual(service.status, 'enabled')
def test_950_instance_count_action(self):
"""Test that action 'instance-count' returns expected values."""
def check_instance_count(expect_count, unit_name):
"""Assert that unit with 'unit_name' has 'expect_count' of VMs.
:param expect_count: How many VMs are expected to be running
:param unit_name: Name of the target nova-compute unit
:return: None
:raises AssertionError: If result of the 'instance-count' action
does not match 'expect_count'.
"""
logging.debug('Running "instance-count" action on unit "{}".'
'Expecting result: {}'.format(unit_name,
expect_count))
result = zaza.model.run_action(unit_name, 'instance-count')
self.assertEqual(result.status, 'completed')
instances = result.data.get('results', {}).get('instance-count')
self.assertEqual(instances, str(expect_count))
nova_unit = zaza.model.get_units('nova-compute',
model_name=self.model_name)[0]
check_instance_count(0, nova_unit.entity_id)
self.RESOURCE_PREFIX = 'zaza-nova'
self.launch_guest(
'ubuntu', instance_key=glance_setup.LTS_IMAGE_NAME)
check_instance_count(1, nova_unit.entity_id)
self.resource_cleanup()
def test_960_remove_from_cloud_actions(self):
"""Test actions remove-from-cloud and register-to-cloud.
Note (martin-kalcok): This test requires that nova-compute unit is not
running any VMs. If there are any leftover VMs from previous tests,
action `remove-from-cloud` will fail.
"""
def wait_for_nova_compute_count(expected_count):
"""Wait for expected number of nova compute services to be present.
Returns True or False based on whether the expected number of nova
compute services was reached within the timeout. Checks are
performed every 10 second in the span of maximum 5 minutes.
"""
sleep_timeout = 1 # don't waste 10 seconds on the first run
for _ in range(31):
sleep(sleep_timeout)
service_list = self.nova_client.services.list(
host=service_name, binary='nova-compute')
if len(service_list) == expected_count:
return True
sleep_timeout = 10
return False
all_units = zaza.model.get_units('nova-compute',
model_name=self.model_name)
unit_to_remove = all_units[0]
service_name = self.fetch_nova_service_hostname(unit_to_remove.name)
registered_nova_services = self.nova_client.services.list(
host=service_name, binary='nova-compute')
service_count = len(registered_nova_services)
if service_count < 1:
self.fail("Unit '{}' has no nova-compute services registered in"
" nova-cloud-controller".format(unit_to_remove.name))
elif service_count > 1:
self.fail("Unexpected number of nova-compute services registered"
" in nova-cloud controller. Expecting: 1, found: "
"{}".format(service_count))
# run action remove-from-cloud and wait for the results in
# nova-cloud-controller
zaza.model.run_action_on_units([unit_to_remove.name],
'remove-from-cloud',
raise_on_failure=True)
# Wait for nova-compute service to be removed from the
# nova-cloud-controller
if not wait_for_nova_compute_count(0):
self.fail("nova-compute service was not unregistered from the "
"nova-cloud-controller as expected.")
# run action register-to-cloud to revert previous action
# and wait for the results in nova-cloud-controller
zaza.model.run_action_on_units([unit_to_remove.name],
'register-to-cloud',
raise_on_failure=True)
if not wait_for_nova_compute_count(1):
self.fail("nova-compute service was not re-registered to the "
"nova-cloud-controller as expected.")
class NovaCompute(NovaCommonTests):
"""Run nova-compute specific tests."""
def test_311_pci_alias_config_compute(self):
"""Verify that the pci alias data is rendered properly.
Change pci-alias and assert that change propagates to the correct
file and that services are restarted as a result
"""
# We are not touching the behavior of anything older than QUEENS
if self.current_release >= self.XENIAL_QUEENS:
self._test_pci_alias_config("nova-compute", ['nova-compute'])
def test_500_hugepagereport_action(self):
"""Test hugepagereport action."""
for unit in zaza.model.get_units('nova-compute',
model_name=self.model_name):
logging.info('Running `hugepagereport` action'
' on unit {}'.format(unit.entity_id))
action = zaza.model.run_action(
unit.entity_id,
'hugepagereport',
model_name=self.model_name,
action_params={})
if "failed" in action.data["status"]:
raise Exception(
"The action failed: {}".format(action.data["message"]))
def test_900_restart_on_config_change(self):
"""Checking restart happens on config change.
Change debug mode and assert that change propagates to the correct
file and that services are restarted as a result
"""
# Config file affected by juju set config change
conf_file = '/etc/nova/nova.conf'
# Make config change, check for service restarts
logging.info('Changing the debug config on nova-compute')
self.restart_on_changed_debug_oslo_config_file(
conf_file,
['nova-compute'])
def test_920_change_aa_profile(self):
"""Test changing the Apparmor profile mode."""
services = ['nova-compute']
set_default = {'aa-profile-mode': 'enforce'}
set_alternate = {'aa-profile-mode': 'complain'}
mtime = zaza.model.get_unit_time(
self.lead_unit,
model_name=self.model_name)
logging.debug('Remote unit timestamp {}'.format(mtime))
with self.config_change(set_default, set_alternate):
logging.info(
'Waiting for services ({}) to be restarted'.format(services))
zaza.model.block_until_services_restarted(
'nova-compute',
mtime,
services,
model_name=self.model_name)
for unit in zaza.model.get_units('nova-compute',
model_name=self.model_name):
logging.info('Checking number of profiles in complain '
'mode in {}'.format(unit.entity_id))
run = zaza.model.run_on_unit(
unit.entity_id,
'aa-status --complaining',
model_name=self.model_name)
output = run['Stdout']
self.assertTrue(int(output) >= len(services))
def test_901_pause_resume(self):
"""Run pause and resume tests.
Pause service and check services are stopped then resume and check
they are started
"""
with self.pause_resume(['nova-compute']):
logging.info("Testing pause resume")
def test_930_check_virsh_default_network(self):
"""Test default virt network is not present."""
for unit in zaza.model.get_units('nova-compute',
model_name=self.model_name):
logging.info('Checking default network is absent on '
'unit {}'.format(unit.entity_id))
run = zaza.model.run_on_unit(
unit.entity_id,
'virsh net-dumpxml default',
model_name=self.model_name)
self.assertFalse(int(run['Code']) == 0)
class NovaComputeActionTest(test_utils.OpenStackBaseTest):
"""Run nova-compute specific tests.
Add this test class for new nova-compute action
to avoid breaking older version
"""
def test_virsh_audit_action(self):
"""Test virsh-audit action."""
for unit in zaza.model.get_units('nova-compute',
model_name=self.model_name):
logging.info('Running `virsh-audit` action'
' on unit {}'.format(unit.entity_id))
action = zaza.model.run_action(
unit.entity_id,
'virsh-audit',
model_name=self.model_name,
action_params={})
if "failed" in action.data["status"]:
raise Exception(
"The action failed: {}".format(action.data["message"]))
class NovaComputeNvidiaVgpuTest(test_utils.OpenStackBaseTest):
"""Run nova-compute-nvidia-vgpu specific tests.
These tests should also turn green if the deployment under test doesn't
have GPU hardware.
"""
def test_vgpu_in_nova_conf(self):
"""Test that nova.conf contains vGPU-related settings.
This test assumes that nova-compute-nvidia-vgpu's config option
vgpu-device-mappings has been set to something not empty like
"{'nvidia-108': ['0000:c1:00.0']}".
"""
for unit in zaza.model.get_units('nova-compute',
model_name=self.model_name):
nova_conf_file = '/etc/nova/nova.conf'
nova_conf = str(generic_utils.get_file_contents(unit,
nova_conf_file))
# See
# https://docs.openstack.org/nova/queens/admin/virtual-gpu.html
# https://docs.openstack.org/nova/ussuri/admin/virtual-gpu.html
# https://docs.openstack.org/releasenotes/nova/xena.html#deprecation-notes
self.assertTrue(('enabled_vgpu_types' in nova_conf) or
('enabled_mdev_types' in nova_conf))
class NovaComputeNvidiaVgpuWithHardwareTest(test_utils.OpenStackBaseTest):
"""Run nova-compute-nvidia-vgpu specific tests.
These tests require real GPU hardware.
"""
def setUp(self):
"""Declare variables that will be used both in tests and tearDown."""
self.RESOURCE_PREFIX = 'zaza-nova'
self.keystone_client = openstack_utils.get_keystone_session_client(
self.keystone_session)
self.trait_name = 'CUSTOM_ZAZA_VGPU'
self.flavor_id = 42
def tearDown(self):
"""Cleanup all created resources."""
self.resource_cleanup() # cleans up the create guests
self._cleanup_vgpu_flavor()
self._cleanup_vgpu_trait()
def test_guest_using_vgpu(self):
"""Test the creation of a guest with a vGPU.
This test assumes that nova-compute-nvidia-vgpu's config option
vgpu-device-mappings has been set to something not empty like
"{'nvidia-108': ['0000:c1:00.0']}".
This test requires OpenStack Stein or newer.
This test performs the following steps:
1. Download the proprietary NVIDIA software.
2. Attach it to the nova-compute-nvidia-vgpu charm as a resource.
3. Reboot the compute nodes.
4. List the available vGPU types.
5. Select a vGPU type via juju config option on the charm.
6. Check the amount of used vGPUs.
7. Create a vGPU trait.
8. Create a flavor with this trait.
9. Create a guest with this flavor.
10. Check the amount of used vGPUs.
"""
package_local_path = self._download_nvidia_package()
self._attach_nvidia_package_as_resource(package_local_path)
self._reboot_vgpu_units()
wanted_vgpu_type = 'nvidia-108'
wanted_gpu_address = '0000:c1:00.0'
self._assert_vgpu_type_available(wanted_vgpu_type, wanted_gpu_address)
logging.info('Selecting vGPU type {} on GPU {} ...'.format(
wanted_vgpu_type, wanted_gpu_address))
alternate_config = {
"vgpu-device-mappings": ("{'" + wanted_vgpu_type + "': ['" +
wanted_gpu_address + "']}")
}
with self.config_change({}, alternate_config, self.application_name,
reset_to_charm_default=True):
self._install_openstack_cli_on_vgpu_units()
resource_provider_id = self._get_vgpu_resource_provider_id(
wanted_gpu_address)
num_vgpu_used_before = self._get_num_vgpu_used(
resource_provider_id)
self._create_vgpu_trait(resource_provider_id)
flavor_name = 'm1.small.vgpu'
self._create_vgpu_flavor(flavor_name)
self._assign_vgpu_trait_to_flavor(flavor_name)
self.launch_guest(
'vgpu', instance_key=glance_setup.LTS_IMAGE_NAME,
flavor_name=flavor_name)
num_vgpu_used_after = self._get_num_vgpu_used(resource_provider_id)
self.assertEqual(num_vgpu_used_after, num_vgpu_used_before + 1)
def _download_nvidia_package(self):
package_cache_dir = tempfile.gettempdir()
package_url = os.environ['TEST_NVIDIA_VGPU_HOST_SW']
package_name = os.path.basename(urllib.parse.urlparse(
package_url).path)
package_local_path = os.path.join(package_cache_dir, package_name)
if not os.path.exists(package_local_path):
logging.info('Downloading {} to {} ...'.format(
package_url, package_local_path))
openstack_utils.download_image(package_url, package_local_path)
else:
logging.info(
'Cached package found at {} - Skipping download'.format(
package_local_path))
return package_local_path
def _get_vgpu_unit_names(self):
vgpu_unit_names = [unit.name for unit in
zaza.model.get_units(self.application_name)]
self.assertGreater(len(vgpu_unit_names), 0, 'No vGPU unit found')
return vgpu_unit_names
def _attach_nvidia_package_as_resource(self, package_local_path):
logging.info('Attaching {} as a resource...'.format(
package_local_path))
zaza.model.attach_resource(self.application_name,
'nvidia-vgpu-software',
package_local_path)
for vgpu_unit_name in self._get_vgpu_unit_names():
zaza.model.block_until_unit_wl_message_match(
vgpu_unit_name, '.*installed NVIDIA software.*')
zaza.model.block_until_all_units_idle()
def _reboot_vgpu_units(self):
vgpu_unit_names = self._get_vgpu_unit_names()
for vgpu_unit_name in vgpu_unit_names:
logging.info('Rebooting {} ...'.format(vgpu_unit_name))
generic_utils.reboot(vgpu_unit_name)
zaza.model.block_until_unit_wl_status(vgpu_unit_name, "unknown")
for vgpu_unit_name in vgpu_unit_names:
zaza.model.block_until_unit_wl_status(vgpu_unit_name, "active")
zaza.model.block_until_all_units_idle()
def _assert_vgpu_type_available(self, wanted_vgpu_type,
wanted_gpu_address):
logging.info(
'Checking that the vGPU type {} is available on GPU {} ...'.format(
wanted_vgpu_type, wanted_gpu_address))
available_vgpu_types = zaza.model.run_action_on_leader(
self.application_name, 'list-vgpu-types',
raise_on_failure=True).results['output']
self.assertIn('{}, {}'.format(wanted_vgpu_type, wanted_gpu_address),
available_vgpu_types)
def _install_openstack_cli_on_vgpu_units(self):
command = 'snap install openstackclients'
for vgpu_unit_name in self._get_vgpu_unit_names():
juju_utils.remote_run(vgpu_unit_name, remote_cmd=command,
timeout=180, fatal=True)
def _get_vgpu_resource_provider_id(self, wanted_gpu_address):
logging.info('Querying resource providers...')
command = (
'openstack {} resource provider list -f value -c uuid -c name')
command = command.format(openstack_utils.get_cli_auth_args(
self.keystone_client))
resource_providers = juju_utils.remote_run(
self._get_vgpu_unit_names()[0], remote_cmd=command, timeout=180,
fatal=True).strip().split('\n')
# At this point resource_providers should look like
# ['0e1379b8-7bd1-40e6-9f41-93cb5b95e38b node-sparky.maas',
# '1bb845a4-cf21-44c2-896e-e877760ad39b \
# node-sparky.maas_pci_0000_c1_00_0']
resource_provider_id = None
wanted_resource_provider_substring = 'pci_{}'.format(
wanted_gpu_address.replace(':', '_').replace('.', '_'))
for resource_provider in resource_providers:
if wanted_resource_provider_substring in resource_provider:
resource_provider_id = resource_provider.split()[0]
self.assertIsNotNone(resource_provider_id)
return resource_provider_id
def _get_num_vgpu_used(self, resource_provider_id):
logging.info('Querying resource provider inventory...')
command = (
'openstack {} resource provider inventory list {} '
'-f value -c used')
command = command.format(openstack_utils.get_cli_auth_args(
self.keystone_client), resource_provider_id)
num_vgpu_used = juju_utils.remote_run(
self._get_vgpu_unit_names()[0], remote_cmd=command, timeout=180,
fatal=True).strip()
return int(num_vgpu_used)
def _create_vgpu_trait(self, resource_provider_id):
logging.info('Creating trait {}...'.format(self.trait_name))
command = (
'openstack {} --os-placement-api-version 1.6 trait create {}')
command = command.format(openstack_utils.get_cli_auth_args(
self.keystone_client), self.trait_name)
first_unit_name = self._get_vgpu_unit_names()[0]
juju_utils.remote_run(first_unit_name, remote_cmd=command, timeout=180,
fatal=True)
command = (
'openstack {} --os-placement-api-version 1.6 resource provider '
'trait set --trait {} {}')
command = command.format(openstack_utils.get_cli_auth_args(
self.keystone_client), self.trait_name, resource_provider_id)
juju_utils.remote_run(first_unit_name, remote_cmd=command, timeout=180,
fatal=True)
def _cleanup_vgpu_trait(self):
logging.info('Cleaning up trait {}...'.format(self.trait_name))
command = (
'openstack {} --os-placement-api-version 1.6 trait delete {}')
command = command.format(openstack_utils.get_cli_auth_args(
self.keystone_client), self.trait_name)
juju_utils.remote_run(
self._get_vgpu_unit_names()[0], remote_cmd=command, timeout=180,
fatal=False)
def _create_vgpu_flavor(self, flavor_name):
logging.info('Creating flavor {}...'.format(flavor_name))
nova_client = openstack_utils.get_nova_session_client(
self.keystone_session)
nova_client.flavors.create(name=flavor_name, ram=2048, vcpus=1,
disk=20, flavorid=self.flavor_id)
def _cleanup_vgpu_flavor(self):
logging.info('Cleaning up created flavor...')
nova_client = openstack_utils.get_nova_session_client(
self.keystone_session)
try:
flavor = nova_client.flavors.get(self.flavor_id)
except novaclient.exceptions.NotFound:
return
nova_client.flavors.delete(flavor)
def _assign_vgpu_trait_to_flavor(self, flavor_name):
logging.info('Assigning trait {} to flavor {} ...'.format(
self.trait_name, flavor_name))
command = (
'openstack {} flavor set {} --property resources:VGPU=1 '
'--property trait:{}=required')
command = command.format(openstack_utils.get_cli_auth_args(
self.keystone_client), flavor_name, self.trait_name)
juju_utils.remote_run(
self._get_vgpu_unit_names()[0], remote_cmd=command, timeout=180,
fatal=True)
class NovaCloudControllerActionTest(test_utils.OpenStackBaseTest):
"""Run nova-cloud-controller specific tests.
Add this test class for new nova-cloud-controller action
to avoid breaking older version.
"""
def test_sync_compute_az_action(self):
"""Test sync-compute-availability-zones action."""
juju_units_az_map = {}
compute_config = zaza.model.get_application_config('nova-compute')
default_az = compute_config['default-availability-zone']['value']
use_juju_az = compute_config['customize-failure-domain']['value']
for unit in zaza.model.get_units('nova-compute',
model_name=self.model_name):
zone = default_az
if use_juju_az:
result = zaza.model.run_on_unit(unit.name,
'echo $JUJU_AVAILABILITY_ZONE',
model_name=self.model_name,
timeout=60)
self.assertEqual(int(result['Code']), 0)
juju_az = result['Stdout'].strip()
if juju_az:
zone = juju_az
juju_units_az_map[zaza.model.get_unit_public_address(unit)] = zone
continue
session = openstack_utils.get_overcloud_keystone_session()
nova = openstack_utils.get_nova_session_client(session)
result = zaza.model.run_action_on_leader(
'nova-cloud-controller',
'sync-compute-availability-zones',
model_name=self.model_name)
# For validating the action results, we simply want to validate that
# the action was completed and we have something in the output. The
# functional validation really occurs below, in that the hosts are
# checked to be in the appropriate host aggregates.
self.assertEqual(result.status, 'completed')
self.assertNotEqual('', result.results['output'])
unique_az_list = list(set(juju_units_az_map.values()))
aggregates = nova.aggregates.list()
self.assertEqual(len(aggregates), len(unique_az_list))
for unit_address in juju_units_az_map:
az = juju_units_az_map[unit_address]
aggregate = nova.aggregates.find(
name='{}_az'.format(az), availability_zone=az)
hypervisor = nova.hypervisors.find(host_ip=unit_address)
self.assertIn(hypervisor.hypervisor_hostname, aggregate.hosts)
class NovaCloudController(NovaCommonTests):
"""Run nova-cloud-controller specific tests."""
@property
def services(self):
"""Return a list of services for the selected OpenStack release."""
services = ['nova-scheduler', 'nova-conductor']
if self.current_release <= self.BIONIC_QUEENS:
services.append('nova-api-os-compute')
if self.current_release <= self.XENIAL_MITAKA:
services.append('nova-cert')
if self.current_release >= self.XENIAL_OCATA:
services.append('apache2')
return services
def test_104_compute_api_functionality(self):
"""Verify basic compute API functionality."""
logging.info('Instantiating nova client...')
keystone_session = openstack_utils.get_overcloud_keystone_session()
nova = openstack_utils.get_nova_session_client(keystone_session)
logging.info('Checking api functionality...')
actual_service_names = [service.to_dict()['binary'] for service in
nova.services.list()]
for expected_service_name in ('nova-scheduler', 'nova-conductor',
'nova-compute'):
assert expected_service_name in actual_service_names
# Thanks to setup.create_flavors we should have a few flavors already:
assert len(nova.flavors.list()) > 0
# Just checking it's not raising and returning an iterable:
assert len(nova.servers.list()) >= 0
def test_106_compute_catalog_endpoints(self):
"""Verify that the compute endpoints are present in the catalog."""
overcloud_auth = openstack_utils.get_overcloud_auth()
keystone_client = openstack_utils.get_keystone_client(
overcloud_auth)
actual_endpoints = keystone_client.service_catalog.get_endpoints()
logging.info('Checking compute endpoints...')
if self.current_release < self.XENIAL_QUEENS:
actual_compute_endpoints = actual_endpoints['compute'][0]
for expected_url in ('internalURL', 'adminURL', 'publicURL'):
assert expected_url in actual_compute_endpoints
else:
actual_compute_interfaces = [endpoint['interface'] for endpoint in
actual_endpoints['compute']]
for expected_interface in ('internal', 'admin', 'public'):
assert expected_interface in actual_compute_interfaces
def test_220_nova_metadata_propagate(self):
"""Verify that the vendor-data settings are propagated.
Change vendor-data-url and assert that change propagates to the correct
file and that services are restarted as a result
"""
if self.current_release < self.BIONIC_ROCKY:
logging.info("Feature didn't exist before Rocky. Nothing to test")
return
# Expected default and alternate values
current_value = zaza.model.get_application_config(
'nova-cloud-controller')['vendor-data-url']['value']
new_value = 'http://some-other.url/vdata'
set_default = {'vendor-data-url': current_value}
set_alternate = {'vendor-data-url': new_value}
default_entry = {'api': {
'vendordata_dynamic_targets': [current_value]}}
alternate_entry = {'api': {'vendordata_dynamic_targets': [new_value]}}
# Config file affected by juju set config change
conf_file = '/etc/nova/nova.conf'
# Make config change, check for service restarts
logging.info(
'Setting config on nova-cloud-controller to {}'.format(
set_alternate))
self.restart_on_changed(
conf_file,
set_default,
set_alternate,
default_entry,
alternate_entry,
self.services)
def test_230_resize_to_the_same_host(self):
"""Verify that the allow-resize-to-same-host setting is propagated.
Change allow-resize-to-same-host and assert that change propagates to
the correct file on nova-cloud-controller units and that services are
restarted as a result. Also verify that the setting is propagated to
the nova-compute units.
"""
services = ['nova-compute']
nova_config_key = 'allow_resize_to_same_host'
juju_config_key = 'allow-resize-to-same-host'
try:
current_value = zaza.model.get_application_config(
'nova-cloud-controller')[juju_config_key]['value']
except KeyError:
logging.info('This version of nova-cloud-controller charm does '
'not suport {} config option. '
'Nothing to test.'.format(juju_config_key))
return
new_value = 'True'
set_default = {juju_config_key: current_value}
set_alternate = {juju_config_key: new_value}
mtime = zaza.model.get_unit_time(
self.lead_unit,
model_name=self.model_name)
logging.debug('Remote unit timestamp {}'.format(mtime))
# Make config change, wait for the services restart on
# nova-cloud-controller
with self.config_change(set_default, set_alternate):
# The config change should propagate to nova-cmopute units
# Wait for them to get settled after relation data has changed
logging.info(
'Waiting for services ({}) to be restarted'.format(services))
zaza.model.block_until_services_restarted(
'nova-compute',
mtime,
services,
model_name=self.model_name)
# Get config value form nova-compute units and verify that
# it's changed
nova_cfg = ConfigParser()
for unit in zaza.model.get_units('nova-compute',
model_name=self.model_name):
logging.info('Checking value of {} in {}'.format(
nova_config_key, unit.entity_id))
result = zaza.model.run_on_unit(
unit.entity_id,
'cat /etc/nova/nova.conf')
nova_cfg.read_string(result['Stdout'])
try:
allow_resize_to_same_host =\
nova_cfg['DEFAULT'][nova_config_key]
logging.debug('Unit {}; value of {} is: {}'.format(
unit.entity_id,
nova_config_key,
allow_resize_to_same_host))
except KeyError:
logging.error('Key {} not found for unit {}'.format(
nova_config_key, unit.entity_id))
allow_resize_to_same_host = None
self.assertEqual(new_value, allow_resize_to_same_host)
def test_302_api_rate_limiting_is_enabled(self):
"""Check that API rate limiting is enabled."""
logging.info('Checking api-paste config file data...')
zaza.model.block_until_oslo_config_entries_match(
'nova-cloud-controller', '/etc/nova/api-paste.ini', {
'filter:legacy_ratelimit': {
'limits': ["( POST, '*', .*, 9999, MINUTE );"]}})
def test_310_pci_alias_config_ncc(self):
"""Verify that the pci alias data is rendered properly.
Change pci-alias and assert that change propagates to the correct
file and that services are restarted as a result
"""
self._test_pci_alias_config("nova-cloud-controller", self.services)
def test_900_restart_on_config_change(self):
"""Checking restart happens on config change.
Change debug mode and assert that change propagates to the correct
file and that services are restarted as a result
"""
# Config file affected by juju set config change
conf_file = '/etc/nova/nova.conf'
# Make config change, check for service restarts
logging.info('Changing debug config on nova-cloud-controller')
self.restart_on_changed_debug_oslo_config_file(
conf_file,
self.services)
def test_901_pause_resume(self):
"""Run pause and resume tests.
Pause service and check services are stopped then resume and check
they are started
"""
with self.pause_resume(self.services):
logging.info("Testing pause resume")
def test_902_quota_settings(self):
"""Verify that the quota settings are propagated.
Change quota-instances and assert that change propagates to the correct
file and that services are restarted as a result
"""
# Expected default and alternate values
current_value = zaza.model.get_application_config(
'nova-cloud-controller')['quota-instances']
try:
current_value = current_value['value']
except KeyError:
current_value = 0
new_value = '20'
set_default = {'quota-instances': current_value}
set_alternate = {'quota-instances': new_value}
expected_conf_section = 'DEFAULT'
expected_conf_key = 'quota_instances'
if self.current_release >= self.XENIAL_OCATA:
expected_conf_section = 'quota'
expected_conf_key = 'instances'
default_entry = {expected_conf_section: {}}
alternate_entry = {expected_conf_section: {
expected_conf_key: [new_value]}}
# Config file affected by juju set config change
conf_file = '/etc/nova/nova.conf'
# Make config change, check for service restarts
logging.info(
'Setting config on nova-cloud-controller to {}'.format(
set_alternate))
self.restart_on_changed(
conf_file,
set_default,
set_alternate,
default_entry,
alternate_entry,
self.services)
def test_903_enable_quota_count_usage_from_placement(self):
"""Verify that quota-count-usage-from-placement is propagated.
Change quota-count-usage-from-placement and assert that nova
configuration file is updated and the services are restarted.
This parameter is not supported for releases<Train. In those
cases assert that nova configuration file is not updated.
"""
# Expected default and alternate values
current_value = zaza.model.get_application_config(
'nova-cloud-controller')['quota-count-usage-from-placement']
try:
current_value = current_value['value']
except KeyError:
current_value = False
new_value = not current_value
new_value_str = str(new_value).title()
current_value_str = str(current_value).title()
set_default = {'quota-count-usage-from-placement': current_value}
set_alternate = {'quota-count-usage-from-placement': new_value}
expected_conf_section = 'quota'
expected_conf_key = 'count_usage_from_placement'
# In case quota-count-usage-from-placement is False, the quota
# section in nova conf file is empty
if current_value:
default_entry = {expected_conf_section: {
expected_conf_key: [current_value_str]}}
alternate_entry = {expected_conf_section: {}}
else:
default_entry = {expected_conf_section: {}}
alternate_entry = {expected_conf_section: {
expected_conf_key: [new_value_str]}}
# Config file affected by juju set config change
conf_file = '/etc/nova/nova.conf'
if self.current_release < self.BIONIC_TRAIN:
# Configuration is not supported in releases<Train
default_entry = {expected_conf_section: {}}
alternate_entry = {expected_conf_section: {}}
services = {}
else:
services = self.services
# Make config change, check for service restarts
logging.info(
'Setting config on nova-cloud-controller to {}'.format(
set_alternate))
self.restart_on_changed(
conf_file,
set_default,
set_alternate,
default_entry,
alternate_entry,
services)
class SecurityTests(test_utils.OpenStackBaseTest):
"""nova-compute and nova-cloud-controller security tests."""
@classmethod
def setUpClass(cls):
"""Run class setup for running Nova SecurityTests."""
super(SecurityTests, cls).setUpClass()
def test_security_checklist(self):
"""Verify expected state with security-checklist."""
# Changes fixing the below expected failures will be made following
# this initial work to get validation in. There will be bugs targeted
# to each one and resolved independently where possible.
expected_failures = [
]
expected_passes = [
'validate-file-ownership',
'validate-file-permissions',
'validate-uses-keystone',
]
tls_checks = [
'validate-uses-tls-for-glance',
'validate-uses-tls-for-keystone',
]
if zaza.model.get_relation_id(
'nova-cloud-controller',
'vault',
remote_interface_name='certificates'):
expected_passes.extend(tls_checks)
else:
expected_failures.extend(tls_checks)
for unit in zaza.model.get_units(self.application_name,
model_name=self.model_name):
logging.info('Running `security-checklist` action'
' on unit {}'.format(unit.entity_id))
test_utils.audit_assertions(
zaza.model.run_action(
unit.entity_id,
'security-checklist',
model_name=self.model_name,
action_params={}),
expected_passes,
expected_failures,
expected_to_pass=not len(expected_failures))