Created
February 19, 2021 13:27
-
-
Save bhcopeland/db655fc2c215812b2edc891812b8897d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import argparse | |
import fnmatch | |
import itertools | |
import os | |
import re | |
import requests | |
import sys | |
from configobj import ConfigObj, ConfigObjError | |
from io import StringIO | |
from string import Template | |
from jinja2 import ( | |
Environment, | |
FileSystemLoader, | |
StrictUndefined, | |
make_logging_undefined, | |
) | |
from jinja2.exceptions import UndefinedError, TemplateSyntaxError | |
from ruamel.yaml import YAML | |
from ruamel.yaml.constructor import ( | |
DuplicateKeyError, | |
ConstructorError, | |
DuplicateKeyFutureWarning, | |
) | |
from ruamel.yaml.scanner import ScannerError | |
from ruamel.yaml.parser import ParserError | |
from ruamel.yaml.composer import ComposerError | |
import logging | |
FORMAT = "[%(funcName)16s() ] %(message)s" | |
logging.basicConfig(level=logging.INFO, format=FORMAT) | |
logger = logging.getLogger(__name__) | |
try: | |
from urllib.parse import urlsplit | |
except ImportError: | |
from urlparse import urlsplit | |
# Templates base path | |
script_dirname = os.path.dirname(os.path.abspath(__file__)) | |
template_base_path = script_dirname | |
testplan_base_path = os.path.join(script_dirname, "testplans/") | |
testcase_base_path = os.path.join(script_dirname, "testcases/") | |
testplan_device_path = os.path.join(script_dirname, "devices/") | |
def parse_template(yaml_string): | |
""" | |
Round trip lava_job through ruamel to test parsing and | |
improve formatting. Comments are preserved. | |
In: yaml-formatted string | |
Out: validated yaml-formatted string | |
""" | |
logger.debug(yaml_string) | |
yaml = YAML() | |
# ruamel does not provide a mechanism to dump to string, so use StringIO | |
# to catch it | |
output = StringIO() | |
yaml.dump(yaml.load(yaml_string), output) | |
# strip empty lines from output | |
return re.sub(r"^\s*$\n", "", output.getvalue(), flags=re.MULTILINE) | |
def get_job_name(lava_job_string): | |
""" | |
In: yaml-formatted string | |
Out: LAVA job's name | |
""" | |
yaml = YAML() | |
lava_job = yaml.load(lava_job_string) | |
return lava_job["job_name"] | |
def _load_template(template_name, template_path, device_type): | |
template = "" | |
template_file_name = "" | |
if template_name: | |
template_file_name = "%s/%s/%s" % (template_path, device_type, template_name) | |
if os.path.exists(template_file_name): | |
with open(template_file_name, "r") as f: | |
template = f.read() | |
else: | |
logger.error( | |
"template (%s) was specified but does not exist" % template_file_name | |
) | |
sys.exit(1) | |
return template, template_file_name | |
def _get_test_plan_list(test_plan_path): | |
""" | |
Returns list of all .yaml files in the directory | |
specified as parameter | |
""" | |
logger.debug("Checking for files in %s" % test_plan_path) | |
ret_list = [] | |
for filename in os.listdir(test_plan_path): | |
if fnmatch.fnmatch(filename, "*.yaml"): | |
ret_list.append(filename) | |
logger.debug(ret_list) | |
return ret_list | |
def _submit_to_squad(lava_job, lava_url_base, qa_server_api, qa_server_base, qa_token): | |
headers = {"Auth-Token": qa_token} | |
try: | |
data = { | |
"definition": lava_job, | |
"backend": urlsplit( | |
lava_url_base | |
).netloc, # qa-reports backends are named as lava instances | |
} | |
logger.info("Submit to: %s" % qa_server_api) | |
results = requests.post(qa_server_api, data=data, headers=headers, timeout=31) | |
if results.status_code < 300: | |
logger.info( | |
"%s/testjob/%s %s" | |
% (qa_server_base, results.text, get_job_name(lava_job)) | |
) | |
else: | |
logger.info(results.status_code) | |
logger.info(results.text) | |
except requests.exceptions.RequestException as err: | |
logger.error("QA Reports submission failed") | |
logger.info("offending job definition:") | |
logger.info(lava_job) | |
sys.exit(1) | |
def _submit_to_lava(lava_job, lava_url_base, lava_username, lava_token): | |
pass | |
def main(): | |
parser = argparse.ArgumentParser() | |
# qa-reports parameters | |
parser.add_argument( | |
"--environment", | |
help="User specified the environment name, prefix or suffix won't be used", | |
dest="environment", | |
default="", | |
) | |
parser.add_argument( | |
"--env-prefix", | |
help="Prefix for the environment name", | |
dest="env_prefix", | |
default="", | |
) | |
parser.add_argument( | |
"--env-suffix", | |
help="Suffix for the environment name", | |
dest="env_suffix", | |
default="", | |
) | |
parser.add_argument( | |
"--build-id", "--build-number", help="ID for the build", dest="build_id" | |
) | |
parser.add_argument( | |
"--qa-server-team", help="Team in QA Reports service", dest="qa_server_team" | |
) | |
parser.add_argument( | |
"--qa-server-project", | |
help="Project in QA Reports service", | |
dest="qa_server_project", | |
) | |
parser.add_argument( | |
"--qa-server", | |
help="QA Reports server", | |
dest="qa_server", | |
default="https://qa-reports.linaro.org", | |
) | |
parser.add_argument( | |
"--qa-token", | |
help="QA Reports token", | |
dest="qa_token", | |
default=os.environ.get("QA_REPORTS_TOKEN"), | |
) | |
# lava parameters | |
parser.add_argument("--lava-server", help="LAVA server URL", dest="lava_server") | |
parser.add_argument("--lava-username", help="LAVA username", dest="lava_username") | |
parser.add_argument( | |
"--lava-token", help="LAVA authentication token", dest="lava_token" | |
) | |
# rendering parameters (test plan) | |
parser.add_argument( | |
"--variables", | |
help="Path to file(s) with variable values", | |
dest="variables", | |
required=True, | |
nargs="+", | |
) | |
parser.add_argument( | |
"--overwrite-variables", | |
help="Key-value pairs overwriting variables from the file", | |
nargs="+", | |
dest="overwrite_variables", | |
default=[], | |
) | |
parser.add_argument( | |
"--device-type", help="Device type in LAVA", dest="device_type", required=True | |
) | |
parser.add_argument( | |
"--template-path", | |
help="Path to LAVA job templates", | |
dest="template_path", | |
default=template_base_path, | |
) | |
parser.add_argument( | |
"--testplan-path", | |
help="Path to directory containing all test plans", | |
dest="testplan_path", | |
default=testplan_base_path, | |
) | |
parser.add_argument( | |
"--testcase-path", | |
help="Path to directory containing all test cases", | |
dest="testcase_path", | |
default=testcase_base_path, | |
) | |
parser.add_argument( | |
"--testplan-device-path", | |
help="Relative path to Jinja2 device deployment fragments", | |
dest="testplan_device_path", | |
default=testplan_device_path, | |
) | |
parser.add_argument( | |
"--test-plan", | |
help="""Directories containing Jinja2 templates to submit for testing. | |
It is assumed that the templates produce valid LAVA job | |
definitions. All variables are substituted using Jinja2 | |
engine""", | |
dest="test_plan", | |
nargs="*", | |
required=False, | |
) | |
parser.add_argument( | |
"--test-case", | |
help="""Specific test cases (as Jinja2 templates) to submit. | |
It is assumed that the templates produce valid LAVA job | |
definitions. All variables are substituted using Jinja2 | |
engine. Multiple --test-case variables are allowed: | |
--test-case test1.yaml test2.yaml --test-case test3.yaml""", | |
dest="test_case", | |
nargs="*", | |
action="append", | |
required=False, | |
) | |
parser.add_argument( | |
"--dry-run", | |
help="""Prepare and write templates to tmp/. | |
Don't submit to actual servers.""", | |
action="store_true", | |
dest="dryrun", | |
) | |
parser.add_argument( | |
"--test-lava-validity", | |
help="""Test generated templates using LAVA container validator""", | |
action="store_true", | |
dest="test_lava_validity", | |
) | |
parser.add_argument( | |
"--verbose", | |
help="""Verbosity level. Follows logging levels: | |
CRITICAL: 50 | |
ERROR: 40 | |
WARNING: 30 | |
INFO: 20 | |
DEBUG: 10 | |
NOTSET: 0""", | |
dest="verbose", | |
type=int, | |
default=logging.INFO, | |
) | |
args = parser.parse_args() | |
logger.setLevel(args.verbose) | |
exit_code = 0 | |
output_path = os.path.join(script_dirname, "tmp") | |
if args.qa_server_project: | |
if "/" in args.qa_server_project: | |
logger.error("--qa-server-project can not contain of a slash in the name") | |
sys.exit(1) | |
if args.dryrun: | |
if not os.path.exists(output_path): | |
os.mkdir(output_path) | |
if args.qa_token is None and args.lava_token is None and not args.dryrun: | |
logger.error("QA_REPORTS_TOKEN and LAVA_TOKEN are missing") | |
sys.exit(1) | |
lava_jobs = [] | |
template_dirs = [ | |
os.path.abspath(template_base_path), | |
os.path.abspath(args.testplan_path), | |
os.path.abspath(args.testcase_path), | |
os.path.abspath(args.testplan_device_path), | |
] | |
# prevent creating templates when variables are missing | |
j2_env = Environment( | |
loader=FileSystemLoader(template_dirs, followlinks=True), | |
undefined=StrictUndefined, | |
) | |
if args.dryrun: | |
LoggingUndefined = make_logging_undefined(logger=logger, base=StrictUndefined) | |
j2_env = Environment( | |
loader=FileSystemLoader(template_dirs, followlinks=True), | |
undefined=LoggingUndefined, | |
) | |
context = {} | |
for variables in args.variables: | |
if not os.path.exists(variables): | |
variables = os.path.join(script_dirname, variables) | |
try: | |
context.update(ConfigObj(variables).dict()) | |
except ConfigObjError: | |
logger.info("Unable to parse .ini file") | |
logger.info("Trying YAML") | |
with open(variables, "r") as vars_file: | |
try: | |
yaml = YAML() | |
context.update(yaml.load(vars_file)) | |
except ParserError as e: | |
logger.error(e) | |
except ComposerError as e: | |
logger.error(e) | |
for variable in args.overwrite_variables: | |
key, value = variable.split("=") | |
context.update({key: value}) | |
context.update({"device_type": args.device_type}) | |
test_list = [] | |
if args.test_plan: | |
for test_plan in args.test_plan: | |
test_plan_path = os.path.abspath( | |
os.path.join(args.testplan_path, test_plan) | |
) | |
for test in _get_test_plan_list(test_plan_path): | |
test_list.append(test) | |
if args.test_case: | |
test_list = test_list + list(itertools.chain.from_iterable(args.test_case)) | |
if len(test_list) == 0: | |
logger.error("No tests matched the given criteria.") | |
sys.exit(1) | |
# convert test_list to set to remove potential duplicates | |
for test in set(test_list): | |
""" Prepare lava jobs """ | |
lava_job = None | |
try: | |
lava_job = j2_env.get_template(test).render(context) | |
lava_job = parse_template(lava_job) | |
lava_jobs.append(lava_job) | |
logger.debug(lava_job) | |
except DuplicateKeyError as e: | |
logger.error(e) | |
exit_code = 1 | |
except ConstructorError as e: | |
logger.error(e) | |
exit_code = 1 | |
except DuplicateKeyFutureWarning as e: | |
logger.error(e) | |
exit_code = 1 | |
except ScannerError as e: | |
logger.error(e) | |
exit_code = 1 | |
except ParserError as e: | |
testpath = os.path.join(output_path, args.device_type, test) | |
logger.error("Failed to parse: %s" % testpath) | |
logger.error(e) | |
exit_code = 1 | |
except TemplateSyntaxError as e: | |
testpath = os.path.join(output_path, args.device_type, test) | |
logger.error("Trying to render: %s" % testpath) | |
logger.error("Error in file: %s" % e.name) | |
logger.error("\tline: %s" % e.lineno) | |
logger.error("\tissue: %s" % e.message) | |
exit_code = 1 | |
except UndefinedError as e: | |
testpath = os.path.join(output_path, args.device_type, test) | |
logger.error("Trying to render: %s" % testpath) | |
logger.error("\tissue: %s" % e.message) | |
exit_code = 1 | |
if args.dryrun and lava_job is not None: | |
testpath = os.path.join( | |
output_path, args.device_type, os.path.basename(test) | |
) | |
logger.info(testpath) | |
if not os.path.exists(os.path.dirname(testpath)): | |
os.makedirs(os.path.dirname(testpath)) | |
with open(os.path.join(testpath), "w") as f: | |
f.write(lava_job) | |
if args.test_lava_validity: | |
import docker | |
client = docker.from_env(version="1.38") | |
logger.debug("Checking for LAVA validity") | |
for test in set(test_list): | |
testpath = os.path.join(os.getcwd(), output_path, args.device_type) | |
logger.debug(testpath) | |
logger.debug(test) | |
container = client.containers.run( | |
image="lavasoftware/lava-server:latest", | |
command="/usr/share/lava-common/lava-schema.py job /data/%s" % test, | |
volumes={"%s" % testpath: {"bind": "/data", "mode": "rw"}}, | |
detach=True, | |
) | |
container_exit_code = container.wait() | |
logger.debug(exit_code) | |
if container_exit_code["StatusCode"] != 0: | |
logger.error("LAVA validation of %s/%s failed" % (testpath, test)) | |
logger.error(container.logs()) | |
exit_code = 1 | |
if not args.dryrun: | |
if not args.qa_server: | |
logger.error("QA-reports server not specified") | |
exit_code = 1 | |
if not args.lava_server: | |
logger.error("Lava server not specified") | |
exit_code = 1 | |
if exit_code != 0: | |
sys.exit(exit_code) | |
qa_server_base = args.qa_server | |
if not ( | |
qa_server_base.startswith("http://") | |
or qa_server_base.startswith("https://") | |
): | |
qa_server_base = "https://" + qa_server_base | |
qa_server_team = args.qa_server_team | |
qa_server_project = args.qa_server_project | |
qa_server_build = args.build_id | |
if not args.environment: | |
# when user not specify value for the environment option, | |
# use the device_type as before | |
qa_server_env = args.env_prefix + args.device_type + args.env_suffix | |
else: | |
# when user specified value for the environment option, | |
# use the user specified value | |
qa_server_env = args.environment | |
qa_server_api = "%s/api/submitjob/%s/%s/%s/%s" % ( | |
qa_server_base, | |
qa_server_team, | |
qa_server_project, | |
qa_server_build, | |
qa_server_env, | |
) | |
lava_server = args.lava_server | |
if not ( | |
lava_server.startswith("http://") or lava_server.startswith("https://") | |
): | |
lava_server = "https://" + lava_server | |
lava_url_base = "%s://%s/" % ( | |
urlsplit(lava_server).scheme, | |
urlsplit(lava_server).netloc, | |
) | |
for lava_job in lava_jobs: | |
""" Submit lava jobs """ | |
if args.qa_token: | |
_submit_to_squad( | |
lava_job, | |
lava_url_base, | |
qa_server_api, | |
qa_server_base, | |
args.qa_token, | |
) | |
if args.lava_token: | |
_submit_to_lava(lava_job, lava_url_base, lava_username, lava_token) | |
else: | |
sys.exit(exit_code) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment