Initial Tower module integration test support.

This commit is contained in:
Matt Clay 2018-03-02 16:32:38 -08:00
parent b9b8081a87
commit b4bf502268
6 changed files with 340 additions and 0 deletions

View file

@ -0,0 +1,18 @@
# This is the configuration template for ansible-test Tower integration tests.
#
# You do not need this template if you are:
#
# 1) Running integration tests without using ansible-test.
# 2) Using the automatically provisioned Tower credentials in ansible-test.
#
# If you do not want to use the automatically provisioned temporary Tower credentials,
# fill in the @VAR placeholders below and save this file without the .template extension.
# This will cause ansible-test to use the given configuration instead of temporary credentials.
#
# NOTE: Automatic provisioning of Tower credentials requires an ansible-core-ci API key.
[general]
version=@VERSION
host=@HOST
username=@USERNAME
password=@PASSWORD

View file

@ -0,0 +1,2 @@
cloud/tower
posix/ci/cloud/group4/tower

View file

@ -0,0 +1,3 @@
- name: Make sure the default Default organization exists
tower_organization:
name: Default

View file

@ -0,0 +1,2 @@
cloud/tower
posix/ci/cloud/group4/tower

View file

@ -0,0 +1,4 @@
- name: Create a Tower team
tower_team:
name: Test Team
organization: Default

View file

@ -0,0 +1,311 @@
"""Tower plugin for integration tests."""
from __future__ import absolute_import, print_function
import os
import time
try:
# noinspection PyPep8Naming
import ConfigParser as configparser
except ImportError:
# noinspection PyUnresolvedReferences
import configparser
from lib.util import (
display,
ApplicationError,
is_shippable,
find_pip,
run_command,
generate_password,
SubprocessError,
)
from lib.cloud import (
CloudProvider,
CloudEnvironment,
)
from lib.core_ci import (
AnsibleCoreCI,
InstanceConnection,
)
from lib.manage_ci import (
ManagePosixCI,
)
from lib.http import (
HttpClient,
)
class TowerCloudProvider(CloudProvider):
"""Tower cloud provider plugin. Sets up cloud resources before delegation."""
def __init__(self, args):
"""
:type args: TestConfig
"""
super(TowerCloudProvider, self).__init__(args, config_extension='.cfg')
self.aci = None
self.version = ''
def filter(self, targets, exclude):
"""Filter out the cloud tests when the necessary config and resources are not available.
:type targets: tuple[TestTarget]
:type exclude: list[str]
"""
if os.path.isfile(self.config_static_path):
return
aci = get_tower_aci(self.args)
if os.path.isfile(aci.ci_key):
return
if is_shippable():
return
super(TowerCloudProvider, self).filter(targets, exclude)
def setup(self):
"""Setup the cloud resource before delegation and register a cleanup callback."""
super(TowerCloudProvider, self).setup()
if self._use_static_config():
self._setup_static()
else:
self._setup_dynamic()
def check_tower_version(self, fallback=None):
"""Check the Tower version being tested and determine the correct CLI version to use.
:type fallback: str | None
"""
tower_cli_version_map = {
'3.1.5': '3.1.8',
'3.2.3': '3.2.1',
}
cli_version = tower_cli_version_map.get(self.version, fallback)
if not cli_version:
raise ApplicationError('Mapping to ansible-tower-cli version required for Tower version: %s' % self.version)
self._set_cloud_config('tower_cli_version', cli_version)
def cleanup(self):
"""Clean up the cloud resource and any temporary configuration files after tests complete."""
# cleanup on success or failure is not yet supported due to how cleanup is called
if self.aci and self.args.remote_terminate == 'always':
self.aci.stop()
super(TowerCloudProvider, self).cleanup()
def _setup_static(self):
config = TowerConfig.parse(self.config_static_path)
self.version = config.version
self.check_tower_version()
def _setup_dynamic(self):
"""Request Tower credentials through the Ansible Core CI service."""
display.info('Provisioning %s cloud environment.' % self.platform, verbosity=1)
# temporary solution to allow version selection
self.version = os.environ.get('TOWER_VERSION', '3.2.3')
self.check_tower_version(os.environ.get('TOWER_CLI_VERSION'))
aci = get_tower_aci(self.args, self.version)
aci.start()
connection = aci.get()
self._set_cloud_config('ssh_hostname', connection.hostname)
self._set_cloud_config('ssh_username', connection.username)
self._set_cloud_config('ssh_port', connection.port)
config = self._read_config_template()
if not self.args.explain:
self.aci = aci
values = dict(
VERSION=self.version,
HOST=connection.hostname,
USERNAME='admin',
PASSWORD=generate_password(),
)
config = self._populate_config_template(config, values)
self._write_config(config)
class TowerCloudEnvironment(CloudEnvironment):
"""Tower cloud environment plugin. Updates integration test environment after delegation."""
def setup(self):
"""Setup which should be done once per environment instead of once per test target."""
self.setup_cli()
if self.managed:
self.setup_dynamic()
self.ping_tower_api()
self.disable_pendo()
def setup_dynamic(self):
"""Dynamic setup which should be done once per environment instead of once per test target."""
display.info('Waiting for Tower instance to become reachable over SSH')
ssh_hostname = self._get_cloud_config('ssh_hostname')
ssh_username = self._get_cloud_config('ssh_username')
ssh_port = self._get_cloud_config('ssh_port')
config = TowerConfig.parse(self.config_path)
aci = get_tower_aci(self.args)
aci.connection = InstanceConnection(True, ssh_hostname, ssh_port, ssh_username, None)
mci = ManagePosixCI(aci)
mci.wait()
display.info('Waiting for Tower to be reconfigured')
attempts = 60
while attempts:
attempts -= 1
try:
# Tower is supposed to drop a /etc/tower/reset/.reconfigured file when it is reconfigured.
# However, the playbook sometimes fails, so we'll look for the completion of the playbook in the log instead.
mci.ssh(['grep', '--quiet', '--no-messages', 'PLAY RECAP', '/etc/tower/reset/reset.log'])
break
except SubprocessError:
time.sleep(5)
else:
raise ApplicationError('Timed out waiting for Tower to be reconfigured.')
display.info('Updating the Tower %s password' % config.username)
cmd = ['awx-manage', 'update_password', '--username', config.username, '--password', config.password]
mci.ssh(cmd)
def setup_cli(self):
"""Install the correct Tower CLI for the version of Tower being tested."""
tower_cli_version = self._get_cloud_config('tower_cli_version')
display.info('Installing Tower CLI version: %s' % tower_cli_version)
pip = find_pip(version=self.args.python_version)
cmd = [pip, 'install', '--disable-pip-version-check', 'ansible-tower-cli==%s' % tower_cli_version]
run_command(self.args, cmd)
def ping_tower_api(self):
"""Wait for Tower API to become available."""
display.info('Waiting for the Tower API to become reachable')
config = TowerConfig.parse(self.config_path)
http = HttpClient(self.args, insecure=True)
http.username = config.username
http.password = config.password
uri = 'https://%s/api/v1/ping/' % config.host
attempts = 60
while attempts:
attempts -= 1
response = http.get(uri)
if response.status_code == 200:
return
time.sleep(5)
raise ApplicationError('Timed out waiting for Tower API to become reachable.')
def disable_pendo(self):
"""Disable Pendo tracking."""
display.info('Disable Pendo tracking')
config = TowerConfig.parse(self.config_path)
# tower-cli does not recognize TOWER_ environment variables
cmd = ['tower-cli', 'setting', 'modify', 'PENDO_TRACKING_STATE', 'off',
'-h', config.host, '-u', config.username, '-p', config.password]
run_command(self.args, cmd)
def configure_environment(self, env, cmd):
"""Configuration which should be done once for each test target.
:type env: dict[str, str]
:type cmd: list[str]
"""
config = TowerConfig.parse(self.config_path)
env.update(config.environment)
class TowerConfig(object):
"""Tower settings."""
def __init__(self, values):
self.version = values.get('version')
self.host = values.get('host')
self.username = values.get('username')
self.password = values.get('password')
if self.password:
display.sensitive.add(self.password)
@property
def environment(self):
"""Tower settings as environment variables.
:rtype: dict[str, str]
"""
env = dict(
TOWER_HOST=self.host,
TOWER_USERNAME=self.username,
TOWER_PASSWORD=self.password,
)
return env
@staticmethod
def parse(path):
"""
:type path: str
:rtype: TowerConfig
"""
parser = configparser.RawConfigParser()
parser.read(path)
keys = (
'version',
'host',
'username',
'password',
)
values = dict((k, parser.get('general', k)) for k in keys)
config = TowerConfig(values)
return config
def get_tower_aci(args, version=None):
"""
:type args: EnvironmentConfig
:type version: str | None
:rtype: AnsibleCoreCI
"""
if version:
persist = True
else:
version = ''
persist = False
return AnsibleCoreCI(args, 'tower', version, persist=persist, stage=args.remote_stage, provider=args.remote_provider)