# # Created by Technology Group # # (c) Copyright, Ubisoft Entertainment Inc. # ALL RIGHTS RESERVED # from __future__ import absolute_import, division, print_function, unicode_literals import argparse import json import logging import os import sys import subprocess import time from utils import cleanup_stadia_processes logger = logging.getLogger(__name__) TIMEOUT = 240 WAIT = 1 log_file_name = 'output_ggp.txt' instance_stdout = '/var/game/stdout' success_string = 'Ubiservices Tests have come to a completion with success' def print_command_exception(command, exception): print("The command [{}] fails with exit code: {} and message: {}".format( command, exception.returncode, exception.output )) def ggp_tool_command(command): try: output = subprocess.check_output(command, universal_newlines=True) except subprocess.CalledProcessError as e: print_command_exception(command, e) raise return output def ggp_tool_command_with_output_print(command): try: # Use subprocess.check_call to allow the process to print the output in the console ret = subprocess.check_call(command, universal_newlines=True) except subprocess.CalledProcessError as e: print_command_exception(command, e) raise return ret def ggp_instance_describe(ggp_exe, instance): """ This function will be used to retrieve some information about an instance. :param ggp_exe: The path to be used for ggp.exe :param instance: The instance name or ID that will be used """ instance_describe_cmd = [ggp_exe, "instance", "describe", instance, "-v", "-s"] return json.loads(ggp_tool_command(instance_describe_cmd)) def ggp_list_all_instances(ggp_exe, pool): """ This function is used to list all the GGP instances. :param ggp_exe: The path to be used for ggp.exe :param pool: The GGP consoles pool. It could miss and in this case a simple instance will be used """ if pool: list_cmd = [ggp_exe, "instance", "list", "-all", "--scope=pool", "--pool='{}'".format(pool)] else: list_cmd = [ggp_exe, "instance", "list", "--all"] return ggp_tool_command(list_cmd) def ggp_config_describe(ggp_exe): """ This function is used to get the project information :param ggp_exe: The path to be used for ggp.exe :return: Project information like organization ID or project ID """ describe_command = [ggp_exe, "config", "describe", "-v", "-s"] return json.loads(ggp_tool_command(describe_command)) def ggp_auto_login(ggp_exe, private_key_path): """ This function is used to make an auto login on the Stadia console using a JSON file. :param ggp_exe: The path to be used for ggp.exe :param private_key_path: The path to the private key JSON file used to make the auto login on Stadia. """ if not private_key_path: raise Exception(" **** The private JSON file used to make the auto login is missing... ****") auto_login_command = [ggp_exe, "auth", "login", "--key-file={}".format(os.path.abspath(private_key_path))] ggp_tool_command(auto_login_command) def reserve_ggp_instance(ggp_exe, instance): """ This function is used to reserve an instance to deploy the application on it :param ggp_exe: The path to be used for ggp.exe :param instance: The name or id of the instance to be reserved """ reserve_cmd = [ggp_exe, "instance", "reserve", instance, "-v"] return ggp_tool_command(reserve_cmd) def reserve_ggp_instance_from_pool(ggp_exe, pool): """ This function is used to reserve a free instance from a consoles pool. :param ggp_exe: The path to be used for ggp.exe :param pool: The id or name of the pool used to reserve a instance """ reserve_from_pool_cmd = [ggp_exe, "reserve", "--pool={}".format(pool), "-v", "-s"] return json.loads(ggp_tool_command(reserve_from_pool_cmd)) def release_ggp_instance(ggp_exe, instance): """ This function is used to release a reserved instance. :param ggp_exe: The path to be used for ggp.exe :param instance: The id or name of the instance to reserve """ release_cmd = [ggp_exe, "instance", "release", instance, "-v"] return ggp_tool_command(release_cmd) def _deploy_application(ggp_exe, deploy_sources, gamelet_id, organization_id, project_id): """ This function is used to deploy the executable application on GGP console. :param ggp_exe: The path to be used for ggp.exe :param deploy_sources: The list of the files than need to be deployed :param gamelet_id: The id or name of the gamelet used to deploy the app :param organization_id: The organization ID :param project_id: The project ID """ for deploy_source in deploy_sources: command = [ggp_exe, "ssh", "put", deploy_source, "--instance={}".format(gamelet_id), "--organization={}".format(organization_id), "--project={}".format(project_id), "-v"] try: subprocess.check_output(command, universal_newlines=True) except subprocess.CalledProcessError as e: print_command_exception(command, e) raise def _run_ggp_application(ggp_exe, test_app_name, test_app_args, application_id, gamelet_id, organization_id, project_id): """ This function is used to run the application on GGP console. :param ggp_exe: The path to be used for ggp.exe :param cmd_args: The command line arguments to pass to the executable :param application_id: The name or ID of the application that will be launched :param gamelet_id: The name of ID of the instance used to run the app :param organization_id: The organization ID :param project_id: The project ID """ run_command = [ggp_exe, "run", "--new-launch-flow=false", '--cmd={} {}'.format(test_app_name, test_app_args), "--output=all", "--headless", "--application={}".format(application_id), "--instance={}".format(gamelet_id), "--organization={}".format(organization_id), "--project={}".format(project_id), "-v", "-s"] logger.info("Running ggp test app with command: {}".format(run_command)) return ggp_tool_command_with_output_print(run_command) def ggp_get_available_test_accounts(ggp_exe): """ This function is used to get all the available test accounts for an Stadia account. :param ggp_exe: The path to be used for ggp.exe :return: Stadia test account name """ test_account_command = [ggp_exe, "test-account", "list", "--all", "-v", "-s"] return json.loads(ggp_tool_command(test_account_command)) def wait_on_instance_available(ggp_exe, instance, timeout=TIMEOUT): """ This function is used to check the ggp instance status """ start = time.time() success = False while time.time() - start < timeout: try: gamelet_status = ggp_instance_describe(ggp_exe, instance) if gamelet_status['state'] not in ('CONFIGURING', 'UNHEALTHY', 'RESERVED'): success = True print("The GGP console seems to be up and ready to be reserved") break elif gamelet_status['state'] == 'RESERVED': return success time.sleep(WAIT) except Exception as e: time.sleep(WAIT) return success def get_organisation_id_and_project_id(ggp_exe): project_info = ggp_config_describe(ggp_exe) # get the organization ID and project ID organization_id = project_info['organizationId'] project_id = project_info['projectId'] return organization_id, project_id def _check_tets_outcome(ggp_exe, what, where, instance, organization_id, project_id): """ This is used in order to check if the tests were successful or not. The "ggp run" command seems to be not changing the error level or taking into account the error level from the gamelet. :param ggp_exe: The path to be used for ggp.exe :param what: What should be extracted from the ggp console :param where: Where should be located :param instance: The name of ID of the instance used to run the app :param organization_id: The organization ID :param project_id:The project ID :return: """ # Retrieve the ggp logs from the gamelet get_command = [ggp_exe, "ssh", "get", what, where, "--scp-flag=-q", "--instance={}".format(instance), "--organization={}".format(organization_id), "--project={}".format(project_id), "-v"] ggp_tool_command(get_command) try: with open(where, encoding="utf8") as logs: # If the success string is not in the logs then raise an exception if not success_string in logs.read(): raise Exception("The tests success string was not found in the logs!") finally: logs.close() os.remove(log_file_name) def parse_arguments(): help_test_app = 'The path to the tested GGP executable.' help_test_app_arguments = 'The arguments given to the test executable.' help_ggp_exe = 'The used GGP command line exe (default is the `ggp` from the PATH)' help_instance = 'The instance ID where the tests will run. If none is specified then the GGP_INSTANCE_ID env variable is used.' help_application = 'The GGP application name that will run. If none is specified then the GGP_APPLICATION env variable is used.' help_private_key_path = 'The private key used for autologin when no instance is specified. If none is specified then the GGP_APPLICATION env variable is used.' help_pool = 'The pool ID used to pick an instance when no instance is specified. If none is specified then the GGP_POOL_ID env variable is used' help_verbose = 'Verbose flag used for debugging.' parser = argparse.ArgumentParser() parser.add_argument('--test_app', help=help_test_app) parser.add_argument('--test_app_arguments', help=help_test_app_arguments) parser.add_argument('--ggp_exe', help=help_ggp_exe, default = 'ggp') parser.add_argument('--instance', help=help_instance, default = os.getenv("GGP_INSTANCE_ID", '')) parser.add_argument('--application', help=help_application, default = os.getenv("GGP_APPLICATION", '')) parser.add_argument('--private_key_path', help=help_private_key_path, default = os.getenv("GGP_PRIVATE_KEY_FILE_PATH", '')) parser.add_argument('--pool', help=help_pool, default = os.getenv("GGP_POOL_ID", '')) parser.add_argument('--verbose', help=help_verbose, action='store_true') args = parser.parse_args() if args.verbose: logging.basicConfig(level=logging.DEBUG, format='%(message)s') logger.info("\nRUNNING WITH ARGS:") logger.info("--test_app={}".format(args.test_app)) logger.info("--test_app_arguments={}".format(args.test_app_arguments)) logger.info("--ggp_exe={}".format(args.ggp_exe)) logger.info("--instance={}".format(args.instance)) logger.info("--application{}=".format(args.application)) logger.info("--private_key_path={}".format(args.private_key_path)) logger.info("--pool={}".format(args.pool)) parsing_failed = False # Validate the given arguments. # The script can't run if the test app or application is not provided. if args.test_app is '': print('ERROR: Missing value for --test_app!\n') parsing_failed = True if args.application is '': print('ERROR: Missing value for --application and the GGP_APPLICATION environment variable was NOT set!\n') parsing_failed = True # Also, it can't run if both instance id and private key or pool id are not provided. if args.instance is '' and (args.private_key_path is '' or args.pool is ''): if args.instance is '': print('ERROR: Missing value for --instance and the GGP_INSTANCE_ID environment variable was NOT set!\n') if args.private_key_path is '': print('ERROR: Missing value for --private_key_path and the GGP_PRIVATE_KEY_FILE_PATH environment variable was NOT set!\n') if args.pool is '': print('ERROR: Missing value for --pool and the GGP_POOL_ID environment variable was NOT set!\n') print('TIP: Either set the --instance parameter to use a specific instance or set the --private_key_path and --pool to pick an available instance from a pool.\n') parsing_failed = True if parsing_failed: parser.print_help() sys.exit(1) return args def main(args): # extracting the 1st param, which is the test EXE to execute test_app_deploy_folder, test_app_name = os.path.split(args.test_app) ggp_login_and_reserve = False try: # check if we already have a GGP console reserved print("Checking reserved console...", end='') instance_info = ggp_instance_describe(args.ggp_exe, args.instance) console_user = instance_info.get('displayName') if console_user in ('', None): print("No console reserved!") print("Performing auto login...") ggp_login_and_reserve = True # each BM should have in the user variables the GGP console IP, GGP pool and also the GGP exe ggp_auto_login(args.ggp_exe, args.private_key_path) # reserve specific gamelet/instance print(ggp_list_all_instances(args.ggp_exe, args.pool)) if args.instance: # we should check if the instance is already reserved check_instance = wait_on_instance_available(args.ggp_exe, args.instance) if not check_instance: raise Exception("The %s instance is unavailable" % args.instance) reserve_ggp_instance(args.ggp_exe, args.instance) elif args.pool: gamelet = reserve_ggp_instance_from_pool(args.ggp_exe, args.pool) args.instance = gamelet['id'] print("Reserved gamelet: {} from pool {}".format(args.instance, args.pool)) else: raise Exception("No instance and no pool ID were specified!") else: print("Console is reserved!") organization_id, project_id = get_organisation_id_and_project_id(args.ggp_exe) deploy_sources = [] # check if there are .so files that need to be deployed for filename in os.listdir(test_app_deploy_folder): if filename.endswith(".so"): deploy_sources.append(os.path.join(test_app_deploy_folder, filename)) deploy_sources.append(args.test_app) # deploy application print("Deploying application...") _deploy_application(args.ggp_exe, deploy_sources, args.instance, organization_id, project_id) # execute tests print("Executing tests...") _run_ggp_application(args.ggp_exe, test_app_name, args.test_app_arguments, args.application, args.instance, organization_id, project_id) # check the tests outcome _check_tets_outcome(args.ggp_exe, instance_stdout, log_file_name, args.instance, organization_id, project_id) except Exception as e: print(e) return 1 finally: if ggp_login_and_reserve: print("Releasing the ggp instance...") release_ggp_instance(args.ggp_exe, args.instance) # kill all stadia processes that are still running after each stack of tests is executed print("Performing cleanup...") cleanup_stadia_processes() return 0 if __name__ == '__main__': args = parse_arguments() exit_code = main(args) sys.exit(exit_code)