import re
import pytest
import random
import attr
from six.moves.urllib.parse import urlparse
from threading import Timer
from cfme.utils import at_exit, conf
# todo: use own logger after logfix merge
from cfme.utils.log import logger as log
from cfme.utils.path import project_path
from .client import SproutClient, SproutException
from cfme.utils.wait import wait_for
_appliance_help = '''specify appliance URLs to use for distributed testing.
this option can be specified more than once, and must be specified at least two times'''
[docs]def pytest_addoption(parser):
group = parser.getgroup("cfme")
group._addoption(
'--appliance', dest='appliances', action='append', metavar='appliance_url',
help=_appliance_help, default=[])
group._addoption('--use-sprout', dest='use_sprout', action='store_true',
default=False, help="Use Sprout for provisioning appliances.")
group._addoption('--sprout-appliances', dest='sprout_appliances', type=int,
default=1, help="How many Sprout appliances to use?.")
group._addoption('--sprout-timeout', dest='sprout_timeout', type=int,
default=60, help="How many minutes is the lease timeout.")
group._addoption('--sprout-provision-timeout', dest='sprout_provision_timeout', type=int,
default=60, help="How many minutes to wait for appliances provisioned.")
group._addoption(
'--sprout-group', dest='sprout_group', default=None, help="Which stream to use.")
group._addoption(
'--sprout-version', dest='sprout_version', default=None, help="Which version to use.")
group._addoption(
'--sprout-date', dest='sprout_date', default=None, help="Which date to use.")
group._addoption(
'--sprout-desc', dest='sprout_desc', default=None, help="Set description of the pool.")
group._addoption('--sprout-override-ram', dest='sprout_override_ram', type=int,
default=0, help="Override RAM (MB). 0 means no override.")
group._addoption('--sprout-override-cpu', dest='sprout_override_cpu', type=int,
default=0, help="Override CPU core count. 0 means no override.")
group._addoption(
'--sprout-provider', dest='sprout_provider', default=None, help="Which provider to use.")
group._addoption('--sprout-provider-type', dest='sprout_provider_type', default=None,
help="Sprout provider type - openshift, etc")
group._addoption('--sprout-template-type', dest='sprout_template_type', default=None,
help="Specifies which template type to use openshift_pod, virtual_machine, docker_vm")
group._addoption('--sprout-ignore-preconfigured', dest='sprout_template_preconfigured',
default=True, action="store_false",
help="Allows to use not preconfigured templates")
[docs]def dump_pool_info(log, pool_data):
log.info("Fulfilled: %s", pool_data["fulfilled"])
log.info("Progress: %s%%", pool_data["progress"])
log.info("Appliances:")
for appliance in pool_data["appliances"]:
name = appliance["name"]
log.info("\t%s:", name)
for key in sorted(appliance.keys()):
if key == "name":
continue
log.info("\t\t%s: %s", key, appliance[key])
[docs]def mangle_in_sprout_appliances(config):
"""
this helper function resets the appliances option of the config and mangles in
the sprout ones
its a hopefully temporary hack until we make a correctly ordered hook for obtaining appliances
"""
provision_request = SproutProvisioningRequest.from_config(config)
mgr = config._sprout_mgr = SproutManager()
requested_appliances = mgr.request_appliances(provision_request)
config.option.appliances[:] = []
appliances = config.option.appliances
log.info("Appliances were provided:")
for appliance in requested_appliances:
appliance_args = {'hostname': appliance['url']}
provider_data = conf.cfme_data['management_systems'].get(appliance['provider'])
if provider_data and provider_data['type'] == 'openshift':
ocp_creds = conf.credentials[provider_data['credentials']]
ssh_creds = conf.credentials[provider_data['ssh_creds']]
extra_args = {
'container': appliance['container'],
'db_host': appliance['db_host'],
'project': appliance['project'],
'openshift_creds': {
'hostname': provider_data['hostname'],
'username': ocp_creds['username'],
'password': ocp_creds['password'],
'ssh': {
'username': ssh_creds['username'],
'password': ssh_creds['password'],
}
}
}
appliance_args.update(extra_args)
appliances.append(appliance_args)
log.info("- %s is %s", appliance['url'], appliance['name'])
mgr.reset_timer()
template_name = requested_appliances[0]["template_name"]
conf.runtime["cfme_data"]["basic_info"]["appliance_template"] = template_name
log.info("appliance_template: %s", template_name)
with project_path.join('.appliance_template').open('w') as template_file:
template_file.write('export appliance_template="{}"'.format(template_name))
log.info("Sprout setup finished.")
config.pluginmanager.register(ShutdownPlugin())
@attr.s
[docs]class SproutProvisioningRequest(object):
"""data holder for provisioning metadata"""
group = attr.ib()
count = attr.ib()
version = attr.ib()
provider = attr.ib()
provider_type = attr.ib()
template_type = attr.ib()
preconfigured = attr.ib()
date = attr.ib()
lease_time = attr.ib()
desc = attr.ib()
provision_timeout = attr.ib()
cpu = attr.ib()
ram = attr.ib()
@classmethod
[docs] def from_config(cls, config):
return cls(
group=config.option.sprout_group,
count=config.option.sprout_appliances,
version=config.option.sprout_version,
provider=config.option.sprout_provider,
provider_type=config.option.sprout_provider_type,
template_type=config.option.sprout_template_type,
preconfigured=config.option.sprout_template_preconfigured,
date=config.option.sprout_date,
lease_time=config.option.sprout_timeout,
desc=config.option.sprout_desc,
provision_timeout=config.option.sprout_provision_timeout,
cpu=config.option.sprout_override_cpu or None,
ram=config.option.sprout_override_ram or None,
)
@attr.s
[docs]class SproutManager(object):
client = attr.ib(default=attr.Factory(SproutClient.from_config))
pool = attr.ib(init=False, default=None)
lease_time = attr.ib(init=False, default=None, repr=False)
timer = attr.ib(init=False, default=None, repr=False)
[docs] def request_appliances(self, provision_request):
self.request_pool(provision_request)
try:
result = wait_for(
self.check_fullfilled,
num_sec=provision_request.provision_timeout * 60,
delay=5,
message="requesting appliances was fulfilled"
)
except Exception:
pool = self.request_check()
dump_pool_info(log, pool)
log.debug("Destroying the pool on error.")
self.destroy_pool()
raise
else:
at_exit(self.destroy_pool)
pool = self.request_check()
dump_pool_info(log, pool)
log.info("Provisioning took %.1f seconds", result.duration)
return pool["appliances"]
[docs] def request_pool(self, provision_request):
log.info("Requesting %s appliances from Sprout at %s",
provision_request.count, self.client.api_entry)
self.lease_time = provision_request.lease_time
if provision_request.desc is not None:
jenkins_job = re.findall(r"Jenkins.*[^\d+$]", provision_request.desc)
if jenkins_job:
self.clean_jenkins_job(jenkins_job)
kargs = {
'count': provision_request.count,
'version': provision_request.version,
'provider': provision_request.provider,
'provider_type': provision_request.provider_type,
'preconfigured': provision_request.preconfigured,
'date': provision_request.date,
'lease_time': provision_request.lease_time,
'cpu': provision_request.cpu,
'ram': provision_request.ram,
}
if provision_request.template_type:
kargs['template_type'] = provision_request.template_type
self.pool = self.client.request_appliances(
provision_request.group, **kargs
)
log.info("Pool %s. Waiting for fulfillment ...", self.pool)
if provision_request.desc is not None:
self.client.set_pool_description(self.pool, provision_request.desc)
[docs] def destroy_pool(self):
try:
self.client.destroy_pool(self.pool)
except Exception:
pass
[docs] def request_check(self):
return self.client.request_check(self.pool)
[docs] def check_fullfilled(self):
try:
result = self.request_check()
except SproutException as e:
# TODO: ensure we only exit this way on sprout usage
self.destroy_pool()
log.error("sprout pool could not be fulfilled\n%s", str(e))
pytest.exit(1)
log.debug("fulfilled at %f %%", result['progress'])
return result["fulfilled"]
[docs] def clean_jenkins_job(self, jenkins_job):
try:
log.info(
"Check if pool already exists for this %r Jenkins job", jenkins_job[0])
jenkins_job_pools = self.client.find_pools_by_description(jenkins_job[0], partial=True)
for pool in jenkins_job_pools:
# Some jobs have overlapping descriptions, sprout API doesn't support regex
# job-name-12345 vs job-name-master-12345
# the partial match alone will catch both of these, use regex to confirm pool
# description is an accurate match
if self.client.get_pool_description(pool) == '{}{}'.format(jenkins_job[0], pool):
log.info("Destroying the old pool %s for %r job.", pool, jenkins_job[0])
self.client.destroy_pool(pool)
else:
log.info('Skipped pool destroy due to potential pool description overlap: %r',
jenkins_job[0])
except Exception:
log.exception(
"Exception occurred during old pool deletion, this can be ignored"
"proceeding to Request new pool")
[docs] def reset_timer(self, timeout=None):
if self.pool is None:
if self.timer:
self.timer.cancel() # Cancel it anyway
log.info("Sprout timer cancelled")
return
if self.timer:
self.timer.cancel()
timeout = timeout or ((self.lease_time / 2) * 60)
self.timer = Timer(timeout, self.ping_pool)
self.timer.daemon = True
self.timer.start()
[docs] def ping_pool(self):
timeout = None # None - keep the half of the lease time
try:
self.client.prolong_appliance_pool_lease(self.pool, self.lease_time)
except SproutException as e:
log.exception(
"Pool %s does not exist any more, disabling the timer.\n"
"This can happen before the tests are shut down "
"(last deleted appliance deleted the pool\n"
"> The exception was: %s", self.pool, str(e))
self.pool = None # Will disable the timer in next reset call.
except Exception as e:
log.error('An unexpected error happened during interaction with Sprout:')
log.exception(e)
# Have a shorter timer now (1 min), because something is happening right now
# WE have a reserve of half the lease time so that should be enough time to
# solve any minor problems
# Adding a 0-10 extra random sec just for sake of dispersing any possible "swarm"
timeout = 60 + random.randint(0, 10)
finally:
self.reset_timer(timeout=timeout)
[docs]def pytest_addhooks(pluginmanager):
pluginmanager.add_hookspecs(NewHooks)
[docs]class ShutdownPlugin(object):
[docs] def pytest_miq_node_shutdown(self, config, nodeinfo):
if config.getoption('ui_coverage'):
# TODO: Ensure this gets called after pytest_sessionfinish
# This disables the appliance deletion when ui coverage is on. ^
# This is because we need one of the appliances to do the collection for us
return
if nodeinfo:
netloc = urlparse(nodeinfo).netloc
ip_address = netloc.split(":")[0]
log.debug("Trying to end appliance {}".format(ip_address))
if config.getoption('--use-sprout'):
try:
call_method = config._sprout_mgr.client.call_method
log.debug("appliance data %r", call_method('appliance_data', ip_address))
log.debug(
"destroy appliance result: %r",
call_method('destroy_appliance', ip_address))
except Exception as e:
log.debug('Error trying to end sprout appliance %s', ip_address)
log.debug(e)
else:
log.debug('Not a sprout run so not doing anything for %s', ip_address)
else:
log.debug('The IP address was not present - not terminating any appliance')
[docs]class NewHooks(object):
[docs] def pytest_miq_node_shutdown(self, config, nodeinfo):
pass