From b9b8081a878c60f620deb1d4f6ed4fe1439799de Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Wed, 7 Mar 2018 14:02:31 -0800 Subject: [PATCH] Cleanup and enhancements for ansible-test. (#37142) * Fix type hint typos. * Add one-time cloud env setup after delegation. * Add generate_password to util. * Add username/password support to HttpClient. * Avoid pip requirement for ansible-test shell. * Support provisioning Tower instances. --- test/runner/lib/cloud/__init__.py | 35 +++++++++++++++++++++++++++++-- test/runner/lib/cloud/azure.py | 4 ++-- test/runner/lib/core_ci.py | 1 + test/runner/lib/executor.py | 18 ++++++++++------ test/runner/lib/http.py | 10 +++++++++ test/runner/lib/manage_ci.py | 2 ++ test/runner/lib/sanity/pylint.py | 6 +++--- test/runner/lib/util.py | 19 +++++++++++++++++ 8 files changed, 82 insertions(+), 13 deletions(-) diff --git a/test/runner/lib/cloud/__init__.py b/test/runner/lib/cloud/__init__.py index f572e9191c9..23d238f7c0d 100644 --- a/test/runner/lib/cloud/__init__.py +++ b/test/runner/lib/cloud/__init__.py @@ -164,6 +164,7 @@ class CloudBase(ABC): _CONFIG_PATH = 'config_path' _RESOURCE_PREFIX = 'resource_prefix' _MANAGED = 'managed' + _SETUP_EXECUTED = 'setup_executed' def __init__(self, args): """ @@ -172,6 +173,20 @@ class CloudBase(ABC): self.args = args self.platform = self.__module__.split('.')[2] + @property + def setup_executed(self): + """ + :rtype: bool + """ + return self._get_cloud_config(self._SETUP_EXECUTED, False) + + @setup_executed.setter + def setup_executed(self, value): + """ + :type value: bool + """ + self._set_cloud_config(self._SETUP_EXECUTED, value) + @property def config_path(self): """ @@ -214,11 +229,15 @@ class CloudBase(ABC): """ self._set_cloud_config(self._MANAGED, value) - def _get_cloud_config(self, key): + def _get_cloud_config(self, key, default=None): """ :type key: str + :type default: str | int | bool | None :rtype: str | int | bool """ + if default is not None: + return self.args.metadata.cloud_config[self.platform].get(key, default) + return self.args.metadata.cloud_config[self.platform][key] def _set_cloud_config(self, key, value): @@ -357,9 +376,21 @@ class CloudProvider(CloudBase): class CloudEnvironment(CloudBase): """Base class for cloud environment plugins. Updates integration test environment after delegation.""" + def setup_once(self): + """Run setup if it has not already been run.""" + if self.setup_executed: + return + + self.setup() + self.setup_executed = True + + def setup(self): + """Setup which should be done once per environment instead of once per test target.""" + pass + @abc.abstractmethod 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] """ diff --git a/test/runner/lib/cloud/azure.py b/test/runner/lib/cloud/azure.py index 69c84a1543a..a28f3e4a60a 100644 --- a/test/runner/lib/cloud/azure.py +++ b/test/runner/lib/cloud/azure.py @@ -171,8 +171,8 @@ class AzureCloudEnvironment(CloudEnvironment): def get_config(config_path): """ - :param config_path: str - :return: dict[str, str] + :type config_path: str + :rtype: dict[str, str] """ with open(config_path, 'r') as config_fd: lines = [line for line in config_fd.read().splitlines() if ':' in line and line.strip() and not line.strip().startswith('#')] diff --git a/test/runner/lib/core_ci.py b/test/runner/lib/core_ci.py index db3a88c85e2..af38d4a4b56 100644 --- a/test/runner/lib/core_ci.py +++ b/test/runner/lib/core_ci.py @@ -69,6 +69,7 @@ class AnsibleCoreCI(object): 'vyos', 'junos', 'ios', + 'tower', ), azure=( 'azure', diff --git a/test/runner/lib/executor.py b/test/runner/lib/executor.py index ed878caf75d..2f2b86973cf 100644 --- a/test/runner/lib/executor.py +++ b/test/runner/lib/executor.py @@ -140,6 +140,9 @@ def install_command_requirements(args): if not args.requirements: return + if isinstance(args, ShellConfig): + return + packages = [] if isinstance(args, TestConfig): @@ -720,6 +723,9 @@ def command_integration_filtered(args, targets, all_targets): tries -= 1 try: + if cloud_environment: + cloud_environment.setup_once() + run_setup_targets(args, test_dir, target.setup_once, all_targets_dict, setup_targets_executed, False) start_time = time.time() @@ -808,12 +814,12 @@ def command_integration_filtered(args, targets, all_targets): def run_setup_targets(args, test_dir, target_names, targets_dict, targets_executed, always): """ - :param args: IntegrationConfig - :param test_dir: str - :param target_names: list[str] - :param targets_dict: dict[str, IntegrationTarget] - :param targets_executed: set[str] - :param always: bool + :type args: IntegrationConfig + :type test_dir: str + :type target_names: list[str] + :type targets_dict: dict[str, IntegrationTarget] + :type targets_executed: set[str] + :type always: bool """ for target_name in target_names: if not always and target_name in targets_executed: diff --git a/test/runner/lib/http.py b/test/runner/lib/http.py index 467105540b2..ac2c6a35e3a 100644 --- a/test/runner/lib/http.py +++ b/test/runner/lib/http.py @@ -42,6 +42,9 @@ class HttpClient(object): self.always = always self.insecure = insecure + self.username = None + self.password = None + def get(self, url): """ :type url: str @@ -83,6 +86,13 @@ class HttpClient(object): headers['Expect'] = '' # don't send expect continue header + if self.username: + if self.password: + display.sensitive.add(self.password) + cmd += ['-u', '%s:%s' % (self.username, self.password)] + else: + cmd += ['-u', self.username] + for header in headers.keys(): cmd += ['-H', '%s: %s' % (header, headers[header])] diff --git a/test/runner/lib/manage_ci.py b/test/runner/lib/manage_ci.py index b66ec97d9a3..defea7f1e37 100644 --- a/test/runner/lib/manage_ci.py +++ b/test/runner/lib/manage_ci.py @@ -135,6 +135,8 @@ class ManagePosixCI(object): self.become = ['sudo', '-in', 'PATH=/usr/local/bin:$PATH'] elif self.core_ci.platform == 'rhel': self.become = ['sudo', '-in', 'bash', '-c'] + elif self.core_ci.platform == 'tower': + self.become = ['sudo', '-in', 'bash', '-c'] def setup(self): """Start instance and wait for it to become ready and respond to an ansible ping.""" diff --git a/test/runner/lib/sanity/pylint.py b/test/runner/lib/sanity/pylint.py index 9715fb69dc8..e41af68e8c8 100644 --- a/test/runner/lib/sanity/pylint.py +++ b/test/runner/lib/sanity/pylint.py @@ -231,9 +231,9 @@ class PylintTest(SanitySingleVersion): def pylint(self, args, context, paths): """ :type args: SanityConfig - :param context: str - :param paths: list[str] - :return: list[dict[str, str]] + :type context: str + :type paths: list[str] + :rtype: list[dict[str, str]] """ rcfile = 'test/sanity/pylint/config/%s' % context diff --git a/test/runner/lib/util.py b/test/runner/lib/util.py index ddeccd45f06..c303d7dba60 100644 --- a/test/runner/lib/util.py +++ b/test/runner/lib/util.py @@ -466,6 +466,25 @@ def is_binary_file(path): return b'\0' in path_fd.read(1024) +def generate_password(): + """Generate a random password. + :rtype: str + """ + chars = [ + string.ascii_letters, + string.digits, + string.ascii_letters, + string.digits, + '-', + ] * 4 + + password = ''.join([random.choice(char) for char in chars[:-1]]) + + display.sensitive.add(password) + + return password + + class Display(object): """Manages color console output.""" clear = '\033[0m'