mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
354 lines
13 KiB
Python
354 lines
13 KiB
Python
"""
|
|
Tests for the Openstack Cloud Provider
|
|
"""
|
|
|
|
|
|
import logging
|
|
import os
|
|
import shutil
|
|
from time import sleep
|
|
|
|
import pytest
|
|
import salt.utils.files
|
|
from salt.config import cloud_config, cloud_providers_config
|
|
from salt.utils.yaml import safe_load
|
|
from tests.support.case import ShellCase
|
|
from tests.support.helpers import random_string
|
|
from tests.support.paths import FILES
|
|
from tests.support.runtests import RUNTIME_VARS
|
|
|
|
TIMEOUT = 500
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
@pytest.mark.expensive_test
|
|
class CloudTest(ShellCase):
|
|
PROVIDER = ""
|
|
REQUIRED_PROVIDER_CONFIG_ITEMS = tuple()
|
|
__RE_RUN_DELAY = 30
|
|
__RE_TRIES = 12
|
|
|
|
@staticmethod
|
|
def clean_cloud_dir(tmp_dir):
|
|
"""
|
|
Clean the cloud.providers.d tmp directory
|
|
"""
|
|
# make sure old provider configs are deleted
|
|
if not os.path.isdir(tmp_dir):
|
|
return
|
|
for fname in os.listdir(tmp_dir):
|
|
os.remove(os.path.join(tmp_dir, fname))
|
|
|
|
def query_instances(self):
|
|
"""
|
|
Standardize the data returned from a salt-cloud --query
|
|
"""
|
|
return {
|
|
x.strip(": ")
|
|
for x in self.run_cloud("--query")
|
|
if x.lstrip().lower().startswith("cloud-test-")
|
|
}
|
|
|
|
def _instance_exists(self, instance_name=None, query=None):
|
|
"""
|
|
:param instance_name: The name of the instance to check for in salt-cloud.
|
|
For example this is may used when a test temporarily renames an instance
|
|
:param query: The result of a salt-cloud --query run outside of this function
|
|
"""
|
|
if not instance_name:
|
|
instance_name = self.instance_name
|
|
if not query:
|
|
query = self.query_instances()
|
|
|
|
log.debug('Checking for "{}" in {}'.format(instance_name, query))
|
|
if isinstance(query, set):
|
|
return instance_name in query
|
|
return any(instance_name == q.strip(": ") for q in query)
|
|
|
|
def assertInstanceExists(self, creation_ret=None, instance_name=None):
|
|
"""
|
|
:param instance_name: Override the checked instance name, otherwise the class default will be used.
|
|
:param creation_ret: The return value from the run_cloud() function that created the instance
|
|
"""
|
|
if not instance_name:
|
|
instance_name = self.instance_name
|
|
|
|
# If it exists but doesn't show up in the creation_ret, there was probably an error during creation
|
|
if creation_ret:
|
|
self.assertIn(
|
|
instance_name,
|
|
[i.strip(": ") for i in creation_ret],
|
|
"An error occured during instance creation: |\n\t{}\n\t|".format(
|
|
"\n\t".join(creation_ret)
|
|
),
|
|
)
|
|
else:
|
|
# Verify that the instance exists via query
|
|
query = self.query_instances()
|
|
for tries in range(self.__RE_TRIES):
|
|
if self._instance_exists(instance_name, query):
|
|
log.debug(
|
|
'Instance "{}" reported after {} seconds'.format(
|
|
instance_name, tries * self.__RE_RUN_DELAY
|
|
)
|
|
)
|
|
break
|
|
else:
|
|
sleep(self.__RE_RUN_DELAY)
|
|
query = self.query_instances()
|
|
|
|
# Assert that the last query was successful
|
|
self.assertTrue(
|
|
self._instance_exists(instance_name, query),
|
|
'Instance "{}" was not created successfully: {}'.format(
|
|
self.instance_name, ", ".join(query)
|
|
),
|
|
)
|
|
|
|
log.debug('Instance exists and was created: "{}"'.format(instance_name))
|
|
|
|
def assertDestroyInstance(self, instance_name=None, timeout=None):
|
|
if timeout is None:
|
|
timeout = TIMEOUT
|
|
if not instance_name:
|
|
instance_name = self.instance_name
|
|
log.debug('Deleting instance "{}"'.format(instance_name))
|
|
delete_str = self.run_cloud(
|
|
"-d {} --assume-yes --out=yaml".format(instance_name), timeout=timeout
|
|
)
|
|
if delete_str:
|
|
delete = safe_load("\n".join(delete_str))
|
|
self.assertIn(self.profile_str, delete)
|
|
self.assertIn(self.PROVIDER, delete[self.profile_str])
|
|
self.assertIn(instance_name, delete[self.profile_str][self.PROVIDER])
|
|
|
|
delete_status = delete[self.profile_str][self.PROVIDER][instance_name]
|
|
if isinstance(delete_status, str):
|
|
self.assertEqual(delete_status, "True")
|
|
return
|
|
elif isinstance(delete_status, dict):
|
|
current_state = delete_status.get("currentState")
|
|
if current_state:
|
|
if current_state.get("ACTION"):
|
|
self.assertIn(".delete", current_state.get("ACTION"))
|
|
return
|
|
else:
|
|
self.assertEqual(current_state.get("name"), "shutting-down")
|
|
return
|
|
# It's not clear from the delete string that deletion was successful, ask salt-cloud after a delay
|
|
query = self.query_instances()
|
|
# some instances take a while to report their destruction
|
|
for tries in range(6):
|
|
if self._instance_exists(query=query):
|
|
sleep(30)
|
|
log.debug(
|
|
'Instance "{}" still found in query after {} tries: {}'.format(
|
|
instance_name, tries, query
|
|
)
|
|
)
|
|
query = self.query_instances()
|
|
# The last query should have been successful
|
|
self.assertNotIn(instance_name, self.query_instances())
|
|
|
|
@property
|
|
def instance_name(self):
|
|
if not hasattr(self, "_instance_name"):
|
|
# Create the cloud instance name to be used throughout the tests
|
|
subclass = self.__class__.__name__.strip("Test")
|
|
# Use the first three letters of the subclass, fill with '-' if too short
|
|
self._instance_name = random_string(
|
|
"cloud-test-{:-<3}-".format(subclass[:3]), uppercase=False
|
|
).lower()
|
|
return self._instance_name
|
|
|
|
@property
|
|
def providers(self):
|
|
if not hasattr(self, "_providers"):
|
|
self._providers = self.run_cloud("--list-providers")
|
|
return self._providers
|
|
|
|
@property
|
|
def provider_config(self):
|
|
if not hasattr(self, "_provider_config"):
|
|
self._provider_config = cloud_providers_config(
|
|
os.path.join(
|
|
RUNTIME_VARS.TMP_CONF_DIR,
|
|
"cloud.providers.d",
|
|
self.PROVIDER + ".conf",
|
|
)
|
|
)
|
|
return self._provider_config[self.profile_str][self.PROVIDER]
|
|
|
|
@property
|
|
def config(self):
|
|
if not hasattr(self, "_config"):
|
|
self._config = cloud_config(
|
|
os.path.join(
|
|
RUNTIME_VARS.TMP_CONF_DIR,
|
|
"cloud.profiles.d",
|
|
self.PROVIDER + ".conf",
|
|
)
|
|
)
|
|
return self._config
|
|
|
|
@property
|
|
def profile_str(self):
|
|
return self.PROVIDER + "-config"
|
|
|
|
def add_profile_config(self, name, data, conf, new_profile):
|
|
"""
|
|
copy the current profile and add a new profile in the same file
|
|
"""
|
|
conf_path = os.path.join(RUNTIME_VARS.TMP_CONF_DIR, "cloud.profiles.d", conf)
|
|
with salt.utils.files.fopen(conf_path, "r") as fp:
|
|
conf = safe_load(fp)
|
|
conf[new_profile] = conf[name].copy()
|
|
conf[new_profile].update(data)
|
|
with salt.utils.files.fopen(conf_path, "w") as fp:
|
|
salt.utils.yaml.safe_dump(conf, fp)
|
|
|
|
def setUp(self):
|
|
"""
|
|
Sets up the test requirements. In child classes, define PROVIDER and REQUIRED_PROVIDER_CONFIG_ITEMS or this will fail
|
|
"""
|
|
super().setUp()
|
|
|
|
if not self.PROVIDER:
|
|
self.fail("A PROVIDER must be defined for this test")
|
|
|
|
# check if appropriate cloud provider and profile files are present
|
|
if self.profile_str + ":" not in self.providers:
|
|
self.skipTest(
|
|
"Configuration file for {0} was not found. Check {0}.conf files "
|
|
"in tests/integration/files/conf/cloud.*.d/ to run these tests.".format(
|
|
self.PROVIDER
|
|
)
|
|
)
|
|
|
|
missing_conf_item = []
|
|
for att in self.REQUIRED_PROVIDER_CONFIG_ITEMS:
|
|
if not self.provider_config.get(att):
|
|
missing_conf_item.append(att)
|
|
|
|
if missing_conf_item:
|
|
self.skipTest(
|
|
"Conf items are missing that must be provided to run these tests: {}".format(
|
|
", ".join(missing_conf_item)
|
|
)
|
|
+ "\nCheck tests/integration/files/conf/cloud.providers.d/{}.conf".format(
|
|
self.PROVIDER
|
|
)
|
|
)
|
|
|
|
def _alt_names(self):
|
|
"""
|
|
Check for an instances created alongside this test's instance that weren't cleaned up
|
|
"""
|
|
query = self.query_instances()
|
|
instances = set()
|
|
for q in query:
|
|
# Verify but this is a new name and not a shutting down ec2 instance
|
|
if q.startswith(self.instance_name) and not q.split("-")[-1].startswith(
|
|
"DEL"
|
|
):
|
|
instances.add(q)
|
|
log.debug(
|
|
'Adding "{}" to the set of instances that needs to be deleted'.format(
|
|
q
|
|
)
|
|
)
|
|
return instances
|
|
|
|
def _ensure_deletion(self, instance_name=None):
|
|
"""
|
|
Make sure that the instance absolutely gets deleted, but fail the test if it happens in the tearDown
|
|
:return True if an instance was deleted, False if no instance was deleted; and a message
|
|
"""
|
|
destroyed = False
|
|
if not instance_name:
|
|
instance_name = self.instance_name
|
|
|
|
if self._instance_exists(instance_name):
|
|
for tries in range(3):
|
|
try:
|
|
self.assertDestroyInstance(instance_name)
|
|
return (
|
|
False,
|
|
'The instance "{}" was deleted during the tearDown, not the test.'.format(
|
|
instance_name
|
|
),
|
|
)
|
|
except AssertionError as e:
|
|
log.error(
|
|
'Failed to delete instance "{}". Tries: {}\n{}'.format(
|
|
instance_name, tries, str(e)
|
|
)
|
|
)
|
|
if not self._instance_exists():
|
|
destroyed = True
|
|
break
|
|
else:
|
|
sleep(30)
|
|
|
|
if not destroyed:
|
|
# Destroying instances in the tearDown is a contingency, not the way things should work by default.
|
|
return (
|
|
False,
|
|
'The Instance "{}" was not deleted after multiple attempts'.format(
|
|
instance_name
|
|
),
|
|
)
|
|
|
|
return (
|
|
True,
|
|
'The instance "{}" cleaned up properly after the test'.format(
|
|
instance_name
|
|
),
|
|
)
|
|
|
|
def tearDown(self):
|
|
"""
|
|
Clean up after tests, If the instance still exists for any reason, delete it.
|
|
Instances should be destroyed before the tearDown, assertDestroyInstance() should be called exactly
|
|
one time in a test for each instance created. This is a failSafe and something went wrong
|
|
if the tearDown is where an instance is destroyed.
|
|
"""
|
|
success = True
|
|
fail_messages = []
|
|
alt_names = self._alt_names()
|
|
for instance in alt_names:
|
|
alt_destroyed, alt_destroy_message = self._ensure_deletion(instance)
|
|
if not alt_destroyed:
|
|
success = False
|
|
fail_messages.append(alt_destroy_message)
|
|
log.error(
|
|
'Failed to destroy instance "{}": {}'.format(
|
|
instance, alt_destroy_message
|
|
)
|
|
)
|
|
self.assertTrue(success, "\n".join(fail_messages))
|
|
self.assertFalse(
|
|
alt_names, "Cleanup should happen in the test, not the TearDown"
|
|
)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.clean_cloud_dir(cls.tmp_provider_dir)
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
# clean up before setup
|
|
cls.tmp_provider_dir = os.path.join(
|
|
RUNTIME_VARS.TMP_CONF_DIR, "cloud.providers.d"
|
|
)
|
|
cls.clean_cloud_dir(cls.tmp_provider_dir)
|
|
|
|
# add the provider config for only the cloud we are testing
|
|
provider_file = cls.PROVIDER + ".conf"
|
|
shutil.copyfile(
|
|
os.path.join(
|
|
os.path.join(FILES, "conf", "cloud.providers.d"), provider_file
|
|
),
|
|
os.path.join(os.path.join(cls.tmp_provider_dir, provider_file)),
|
|
)
|