366 lines
15 KiB
Python
366 lines
15 KiB
Python
#
|
|
# 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)
|