lqa-20180227.0/0000755000175000017500000000000013245261463013200 5ustar zumbizumbi00000000000000lqa-20180227.0/PKG-INFO0000644000175000017500000000046513245261463014302 0ustar zumbizumbi00000000000000Metadata-Version: 1.0 Name: lqa Version: 20180227.0 Summary: LAVA QA command line tool Home-page: http://cgit.collabora.com/git/singularity/tools/lqa.git/ Author: Luis Araujo Author-email: luis.araujo@collabora.co.uk License: LGPL-2.1+ Description-Content-Type: UNKNOWN Description: UNKNOWN Platform: UNKNOWN lqa-20180227.0/lqa.egg-info/0000755000175000017500000000000013245261463015447 5ustar zumbizumbi00000000000000lqa-20180227.0/lqa.egg-info/requires.txt0000644000175000017500000000002713245261463020046 0ustar zumbizumbi00000000000000PyYaml jinja2 requests lqa-20180227.0/lqa.egg-info/PKG-INFO0000644000175000017500000000046513245261463016551 0ustar zumbizumbi00000000000000Metadata-Version: 1.0 Name: lqa Version: 20180227.0 Summary: LAVA QA command line tool Home-page: http://cgit.collabora.com/git/singularity/tools/lqa.git/ Author: Luis Araujo Author-email: luis.araujo@collabora.co.uk License: LGPL-2.1+ Description-Content-Type: UNKNOWN Description: UNKNOWN Platform: UNKNOWN lqa-20180227.0/lqa.egg-info/SOURCES.txt0000644000175000017500000000226013245261463017333 0ustar zumbizumbi00000000000000README lqa setup.py lqa.egg-info/PKG-INFO lqa.egg-info/SOURCES.txt lqa.egg-info/dependency_links.txt lqa.egg-info/not-zip-safe lqa.egg-info/requires.txt lqa.egg-info/top_level.txt lqa_api/__init__.py lqa_api/analyse.py lqa_api/connection.py lqa_api/exit_codes.py lqa_api/job.py lqa_api/outputlog.py lqa_api/test.py lqa_api/utils.py lqa_api/waitqueue.py lqa_tests/__init__.py lqa_tests/test_cli.py lqa_tests/test_merge_profiles.py lqa_tests/test_replace_key_fields.py lqa_tool/__init__.py lqa_tool/exceptions.py lqa_tool/settings.py lqa_tool/utils.py lqa_tool/version.py lqa_tool/commands/__init__.py lqa_tool/commands/cancel.py lqa_tool/commands/cleanqueue.py lqa_tool/commands/devices.py lqa_tool/commands/diffresults.py lqa_tool/commands/job.py lqa_tool/commands/jobdef.py lqa_tool/commands/liststreams.py lqa_tool/commands/maint.py lqa_tool/commands/mkstream.py lqa_tool/commands/online.py lqa_tool/commands/output.py lqa_tool/commands/queue.py lqa_tool/commands/report.py lqa_tool/commands/resubmit.py lqa_tool/commands/results.py lqa_tool/commands/status.py lqa_tool/commands/submit.py lqa_tool/commands/sversion.py lqa_tool/commands/test.py lqa_tool/commands/wait.py lqa_tool/commands/whoami.pylqa-20180227.0/lqa.egg-info/top_level.txt0000644000175000017500000000003313245261463020175 0ustar zumbizumbi00000000000000lqa_api lqa_tests lqa_tool lqa-20180227.0/lqa.egg-info/not-zip-safe0000644000175000017500000000000113245261463017675 0ustar zumbizumbi00000000000000 lqa-20180227.0/lqa.egg-info/dependency_links.txt0000644000175000017500000000000113245261463021515 0ustar zumbizumbi00000000000000 lqa-20180227.0/lqa0000755000175000017500000000221013245237772013704 0ustar zumbizumbi00000000000000#!/usr/bin/env python ################################################################################### # LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_api.exit_codes import SUCCESS from lqa_tool import Cli def main(): lqa = Cli() lqa.run() if __name__ == '__main__': main() exit(SUCCESS) lqa-20180227.0/lqa_api/0000755000175000017500000000000013245261463014606 5ustar zumbizumbi00000000000000lqa-20180227.0/lqa_api/outputlog.py0000644000175000017500000001025313245237772017231 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool - Class to fetch job output logs # Copyright (C) 2017 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import sys import time from xmlrpclib import Fault from lqa_api.job import Job class OutputLogError(Exception): """Base exception for Output Logs errors""" def __init__(self, msg, code): self.msg = msg self.code = code def __str__(self): return "{} {}".format(self.msg, self.code) class OutputLog(object): def __init__(self, job_id, server, is_live=False, outfile=None, logger=None): self._job_id = job_id self._server = server self._is_live = is_live self._outfile = outfile self._logger = logger def run(self): job = Job(self._job_id, self._server) if self._is_live: # If is_live, read incrementing offset of the output to show live. # Wait for the job to start if it is in a submitted state. if job.is_submitted(): if self._logger: self._logger.info("Waiting for job to start: {}".format(job)) while job.is_submitted(): time.sleep(1) offset = 0 while job.is_running(): offset = self._fetch_and_write_output(offset, job) time.sleep(0.5) # Run one more time to catch any left data from the time.sleep delay. # This call also helps to fetch the output for non-running states. self._fetch_and_write_output(offset, job) else: # Otherwise just fetch and show the output file at once. try: output_data = job.output() if self._outfile: with open(self._outfile, 'w+') as f: f.write(output_data) else: # Print to stdout sys.stdout.write(output_data) sys.stdout.flush() except Fault as e: # 404 Job output not found. if e.faultCode == 404: if self._logger: self._logger.error("{}: Status for job '{}': {}" \ .format(e.faultString, job, job.status)) elif self._logger: self._logger.error("output: error: {}".format(e)) raise OutputLogError(e.faultString, e.faultCode) def _fetch_and_write_output(self, offset, job): try: output_data = job.output(offset) if output_data: sys.stdout.write(output_data) offset = offset + len(output_data) except Fault as e: if e.faultCode == 404: # Skip this error code only during the short time when the job # started running but the log is not yet available. if job.is_running(): return offset if self._logger: self._logger.error("{}: Status for job '{}': {}" \ .format(e.faultString, job, job.status)) else: if self._logger: self._logger.error("output: error: {}".format(e)) raise OutputLogError(e.faultString, e.faultCode) # Everytihng went fine, just return offset return offset lqa-20180227.0/lqa_api/test.py0000644000175000017500000001231713245237772016151 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA api - This class represents a LAVA test object. # Copyright (C) 2015 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### class Test(object): """This class represents a LAVA test""" def __init__(self, test, is_v2=True): self._is_v2 = is_v2 if is_v2: self._name = test.get('name') else: self._name = test.get('test_id') self._test = test self._results = test.get('test_results') self._uuid = test.get('analyzer_assigned_uuid') self._attributes = test.get('attributes') self._attachments = test.get('attachments') self._software_ctx = test.get('software_context') self._hardware_ctx = test.get('hardware_context') self._testdef_metadata = test.get('testdef_metadata') def __str__(self): if self._is_v2: return "{}: {}".format(self._name, self.job) else: return "{}: {}".format(self._name, self._uuid) def is_v2(self): return self._is_v2 @property def name(self): return self._name # LAVA V2 Test properties @property def result(self): return self._test.get('result') @property def job(self): return self._test.get('job') @property def suite(self): return self._test.get('suite') @property def url(self): return self._test.get('url') @property def logged(self): return self._test.get('logged') @property def unit(self): return self._test.get('unit') @property def duration(self): return self._test.get('duration') @property def timeout(self): return self._test.get('timeout') @property def measurement(self): return self._test.get('measurement') @property def level(self): return self._test.get('level') @property def metadata(self): return self._test.get('metadata') # LAVA V1 Test properties @property def uuid(self): return self._uuid @property def results(self): return self._results @property def attributes(self): return self._attributes @property def attachments(self): return self._attachments @property def params(self): if self._software_ctx: return self._software_ctx['sources'][0]['test_params'] @property def project(self): if self._software_ctx: return self._software_ctx['sources'][0]['project_name'] @property def image(self): if self._software_ctx: return self._software_ctx['image']['name'] @property def packages(self): if self._software_ctx: return self._software_ctx['packages'] @property def branch_url(self): if self._software_ctx: return self._software_ctx['sources'][0]['branch_url'] @property def branch_rev(self): if self._software_ctx: return self._software_ctx['sources'][0]['branch_revision'] @property def branch_vcs(self): if self._software_ctx: return self._software_ctx['sources'][0]['branch_vcs'] @property def testdef_metadata(self): return self._testdef_metadata @property def version(self): if self._testdef_metadata: return self._testdef_metadata['version'] @property def description(self): if self._testdef_metadata: return self._testdef_metadata['description'] @property def format(self): if self._testdef_metadata: return self._testdef_metadata['format'] @property def location(self): if self._testdef_metadata: return self._testdef_metadata['location'] @property def devices(self): if self._hardware_ctx: return self._hardware_ctx['devices'] # Convenient functions to filter tests results. @property def results_passed(self): return self._filter_result('pass') @property def results_failed(self): return self._filter_result('fail') @property def results_skipped(self): return self._filter_result('skip') @property def results_unknown(self): return self._filter_result('unknown') def _filter_result(self, result): if not self._is_v2: return filter(lambda tc: tc['result'] == result, self.results) lqa-20180227.0/lqa_api/utils.py0000644000175000017500000000502113245237772016324 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool - Utility API module. # Copyright (C) 2016 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### class RangeNotationError(Exception): pass def job_id_list_from_range(jobs_range): """This function builds a job id list from a range of jobs ids. Many of the commands accept job id's as arguments, these can be given individually 'Id0 Id1 .. IdN' or with a range notation like 'Id0-IdN', or even a combination of both notations 'Id0 Id0-IdN Id1 Id1-IdN Id3'. This function takes into consideration such a special notation and generates a proper list of id's (of integer type) that can be later easily used by each of the commands, so this function also converts the job id's from string type (as received by argparser) to an integer type.""" job_ids_list = [] try: for job_id in jobs_range: job_ids = job_id.split('-', 2) n = len(job_ids) if n == 1: job_ids_list.append(int(job_ids[0])) elif n == 2: # If job_id_value is == 2 then it is a range. frm, to = int(job_ids[0]), int(job_ids[1]) if frm > to: # Go backward if from > to. job_ids_list.extend(range(frm, to - 1, -1)) else: job_ids_list.extend(range(frm, to + 1)) else: raise RangeNotationError("job range error: not a valid range: {}" .format(job_ids)) # Catch all the exceptions and raise them as RangeNotationError except Exception as e: raise RangeNotationError("job range error: {}".format(e)) return job_ids_list lqa-20180227.0/lqa_api/connection.py0000644000175000017500000001040713245237772017327 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015, 2016 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from xmlrpclib import ServerProxy class Connection(object): """Class handling the connection to the LAVA server.""" def __init__(self, api_url): self._api_url = api_url self._conn = ServerProxy(self._api_url, use_datetime=True, allow_none=True) def submit_job(self, data): return self._conn.scheduler.submit_job(data) def cancel_job(self, job_id): return self._conn.scheduler.cancel_job(job_id) def resubmit_job(self, job_id): return self._conn.scheduler.resubmit_job(job_id) def streams(self): return self._conn.dashboard.streams() def make_stream(self, pathname, description): return self._conn.dashboard.make_stream(pathname, description) def job_details(self, job_id): return self._conn.scheduler.job_details(job_id) def job_output(self, job_id, offset=0): return self._conn.scheduler.job_output(job_id, offset).data def job_status(self, job_id): return self._conn.scheduler.job_status(job_id) def get_device_status(self, hostname): return self._conn.scheduler.get_device_status(hostname) def get_bundle(self, sha1): return self._conn.dashboard.get(sha1) def get_testjob_metadata(self, job_id): return self._conn.results.get_testjob_metadata(job_id) def get_testjob_results_csv(self, job_id): return self._conn.results.get_testjob_results_csv(job_id) def get_testjob_results_yaml(self, job_id): return self._conn.results.get_testjob_results_yaml(job_id) def get_testsuite_results_csv(self, job_id, suite_name): return self._conn.results.get_testsuite_results_csv(job_id, suite_name) def get_testsuite_results_yaml(self, job_id, suite_name): return self._conn.results.get_testsuite_results_yaml(job_id, suite_name) def get_testcase_results_csv(self, job_id, suite_name, case_name): return self._conn.results.get_testcase_results_csv(job_id, suite_name, case_name) def get_testcase_results_yaml(self, job_id, suite_name, case_name): return self._conn.results.get_testcase_results_yaml(job_id, suite_name, case_name) def get_filter_results(self, filter_name, count=10, offset=0): return self._conn.dashboard.get_filter_results(filter_name, count, offset) def get_filter_results_since(self, filter_name, since): return \ self._conn.dashboard.get_filter_results_since(filter_name, since) def bundles(self, pathname): return self._conn.dashboard.bundles(pathname) def all_jobs(self): return self._conn.scheduler.all_jobs() def all_devices(self): return self._conn.scheduler.all_devices() def put_into_maintenance_mode(self, hostname, reason, notify): return self._conn.scheduler.put_into_maintenance_mode(hostname, reason, notify) def put_into_online_mode(self, hostname, reason, skip_health_check): return self._conn.scheduler.put_into_online_mode(hostname, reason, skip_health_check) def whoami(self): return self._conn.system.whoami() def version(self): return self._conn.dashboard.version() lqa-20180227.0/lqa_api/analyse.py0000644000175000017500000000704613245237772016631 0ustar zumbizumbi00000000000000################################################################################### # LQA Analyse: This module is used to fetch and analyse LAVA jobs/tests results. # # Copyright (C) 2015 Collabora Ltd. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import json import os.path from lqa_api.job import Job class Analyse(object): def __init__(self, ids, conn): self._ids = ids self._conn = conn self._complete = { 'successful': [], 'failed': [] } self._incomplete = { 'bundles': [], 'no_bundles': [] } self._pending = { 'submitted': [], 'running': [] } self._canceled = { 'canceled': [], 'canceling': [] } def _find_jobs_status(self): for job_id in self._ids: job = Job(job_id, self._conn) if job.is_submitted(): self._pending['submitted'].append(job) elif job.is_running(): self._pending['running'].append(job) elif job.is_canceled(): self._canceled['canceled'].append(job) elif job.is_canceling(): self._canceled['canceling'].append(job) elif job.is_complete(): self._find_successful_or_failed(job) else: if job.has_bundle(): self._incomplete['bundles'].append(job) else: self._incomplete['no_bundles'].append(job) def _find_successful_or_failed(self, job): for test in job.tests: if test.results_failed: self._complete['failed'].append(job) # Return as soon as a failed test is found for the job. return # If no failed test in the job then it is successful. self._complete['successful'].append(job) def _find_missing_tests(self, job, test_run_ids): job_definition = json.loads(job.job_definition) test_def_list = [] for actions in job_definition['actions']: if actions['command'] == 'lava_test_shell': params = actions['parameters'] # Collect the test definitions from the job file so # that it can be compared with the final list of test runs. for td in params.get('testdef_repos', []): test_def_list.append( os.path.splitext(os.path.basename(td['testdef']))[0]) for v in test_run_ids: # Skip the 'lava' test from the bundle if v == 'lava': continue test_def_list.remove(v) return test_def_list def sort_jobs_by_status(self): self._find_jobs_status() return { 'complete': self._complete, 'incomplete': self._incomplete, 'pending': self._pending, 'canceled': self._canceled, } lqa-20180227.0/lqa_api/job.py0000644000175000017500000001634013245237772015744 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA API - This class represents a LAVA job object. # Copyright (C) 2015, 2016 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import json import yaml from xmlrpclib import Fault from lqa_api.test import Test from lqa_api.exit_codes import SUCCESS, \ EXIT_CODE_INCOMPLETE_JOB, \ EXIT_CODE_CANCELED_JOB class JobError(Exception): """This is the base exception for all Job errors.""" def __init__(self, msg, code): self.msg = msg self.code = code def __str__(self): return "{} {}".format(self.msg, self.code) class Job(object): def __init__(self, job_id, server): self._id = job_id self._server = server self._status = {} self._bundle = {} self._tests = [] self._details = self._get_job_details() # If it is a pipeline job then it is version 2 self._is_v2 = self._details['is_pipeline'] def __str__(self): return "{} - {}".format(self._id, self.name) def _fetch_job_status(self): """Method to fetch the job status""" try: self._status = self._server.job_status(self._id) except Fault as e: raise JobError(e.faultString, e.faultCode) def _get_job_details(self): """Method to fetch the job details""" try: return self._server.job_details(self._id) except Fault as e: raise JobError(e.faultString, e.faultCode) def _fetch_job_bundle(self): """Fetch job bundle""" if not self._bundle: try: bundle = self._server.get_bundle(self.sha1) self._bundle = json.loads(bundle['content']) except Fault as e: raise JobError(e.faultString, e.faultCode) def _fetch_tests(self): if not self._tests: # The way tests are fetched is different for jobs V1 and V2. if self.is_v2(): tests_yaml = self._server.get_testjob_results_yaml(self.id) for test_record in yaml.load(tests_yaml): self._tests.append(Test(test_record)) else: for test_run in self.test_runs: self._tests.append(Test(test_run, is_v2=False)) def _get_job_details_prop(self, prop): """Get property prop from job details metadata""" return self._details.get(prop, None) # External API @property def id(self): return self._id @property def bundle(self): self._fetch_job_bundle() return self._bundle @property def details(self): return self._details @property def name(self): return self._get_job_details_prop('description') @property def bundle_link(self): return self._get_job_details_prop('_results_link') @property def priority(self): return { None : None, 0 : "low", 50 : "medium", 100 : "high" }[self._get_job_details_prop('priority')] @property def submit_time(self): return self._get_job_details_prop('submit_time') @property def start_time(self): return self._get_job_details_prop('start_time') @property def end_time(self): return self._get_job_details_prop('end_time') @property def device_type(self): return self._get_job_details_prop('requested_device_type_id') @property def job_definition(self): return self._get_job_details_prop('definition') @property def hostname(self): dc = self._get_job_details_prop('_actual_device_cache') if dc: return dc.get('hostname', None) @property def worker_host(self): dc = self._get_job_details_prop('_actual_device_cache') if dc: return dc.get('worker_host_id', None) @property def failure_comment(self): return self._get_job_details_prop('failure_comment') @property def sha1(self): self._fetch_job_status() return self._status.get('bundle_sha1', None) @property def status(self): self._fetch_job_status() return self._status.get('job_status', None) @property def test_runs(self): self._fetch_job_bundle() return self._bundle.get('test_runs', None) @property def tests(self): self._fetch_tests() return self._tests def output(self, offset=0): return self._server.job_output(self.id, offset) def has_bundle(self): return self.bundle_link is not None def is_v2(self): return self._is_v2 # Test for the different job statuses def is_submitted(self): return self.status == 'Submitted' def is_running(self): return self.status == 'Running' def is_complete(self): return self.status == 'Complete' def is_incomplete(self): return self.status == 'Incomplete' def is_canceled(self): return self.status == 'Canceled' def is_canceling(self): return self.status == 'Canceling' @property def exit_code(self): """This function maps job status to a command exit code""" ec = SUCCESS if self.is_incomplete(): ec = EXIT_CODE_INCOMPLETE_JOB elif self.is_canceled() or self.is_canceling(): ec = EXIT_CODE_CANCELED_JOB return ec @property def results(self): """ This property gets the list of all the test results for a job with the following format per line: :::: It calls the correct method for jobs version 1 or 2. """ return (self._is_v2 and self._results_v2()) or self._results_v1() def _results_v2(self): """This method fetches results for LAVA jobs version 2""" results_lines = [] for test in self.tests: results_lines.append("{}:{}:{}:{}:{}".format( self.id, self.name, test.suite, test.name, test.result)) return results_lines def _results_v1(self): """This method fetches results for LAVA jobs version 1""" results_lines = [] for test in self.tests: for test_result in test.results: results_lines.append("{}:{}:{}:{}:{}".format( self.id, self.name, test.name, test_result['test_case_id'], test_result['result'])) return results_lines lqa-20180227.0/lqa_api/exit_codes.py0000644000175000017500000000241513245237772017316 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA API - Exit codes # Copyright (C) 2015, 2016 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### # Everything went well SUCCESS=0 # A failure was reported by an operation (e.g a failed test was reported) OPERATION_FAILED=1 # The application failed with an error APPLICATION_ERROR=2 # These codes are used to determinate command exit code depending on job status. EXIT_CODE_CANCELED_JOB = -1 EXIT_CODE_INCOMPLETE_JOB = -2 lqa-20180227.0/lqa_api/__init__.py0000644000175000017500000000000013245237772016713 0ustar zumbizumbi00000000000000lqa-20180227.0/lqa_api/waitqueue.py0000644000175000017500000000726613245237772017212 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import time from lqa_api.exit_codes import SUCCESS # Default to a 3 hours timeout WAIT_DEFAULT_TIMEOUT="3h" class WaitQueue(list): """Wait queue for jobs. It is a subclass of the list object.""" def __init__(self): # Default queue exit code is SUCCESS which means no job in the queue failed self._queue_exit_code = SUCCESS def addjob(self, job, add_cb=None): """Add job to the wait queue.""" self.append(job) add_cb and add_cb(self, job) def removejob(self, job, remove_cb=None): """Remove job from the wait queue.""" self.remove(job) # Update queue exit code self._update_exit_code(job) remove_cb and remove_cb(self, job) def has_jobs(self): """Check if there are pending jobs in the wait queue.""" return len(self) > 0 def wait(self, readable_timeout=WAIT_DEFAULT_TIMEOUT, wait_cb=None, remove_cb=None, timeout_cb=None): """Wait for the jobs on Submitted and Running state.""" timeout = _convert_readable_timeout_to_seconds(readable_timeout) endtime = None # Negative value unset the timeout. if timeout >= 0: endtime = time.time() + timeout while True: for job in self[:]: if job.status in ['Submitted', 'Running']: # Call the wait function (if specified). wait_cb and wait_cb(self, job) else: self.removejob(job, remove_cb) # Break if no more jobs to wait for. if not self.has_jobs(): break # Exit with timeout of endtime. if endtime and time.time() > endtime: # Call the timeout function (if specified). timeout_cb and timeout_cb(self, timeout) break # Sleep for 30 seconds before polling again. time.sleep(30) # Return the queue. return self def _update_exit_code(self, job): """This function collates the queue exit code to the worst error code""" ec = job.exit_code if ec < self._queue_exit_code: self._queue_exit_code = ec @property def exit_code(self): return self._queue_exit_code def _convert_readable_timeout_to_seconds(timeout): """ Supported notations: h = hours m = minutes s = seconds (default if not specified) """ # 1 minute = 60 secs minute = 60 hour = minute * minute return \ timeout.endswith('h') and float(timeout[:-1]) * hour or \ timeout.endswith('m') and float(timeout[:-1]) * minute or \ timeout.endswith('s') and float(timeout[:-1]) or \ float(timeout) lqa-20180227.0/setup.cfg0000644000175000017500000000004613245261463015021 0ustar zumbizumbi00000000000000[egg_info] tag_build = tag_date = 0 lqa-20180227.0/README0000644000175000017500000000473313245237772014075 0ustar zumbizumbi00000000000000== LAVA QA Tool == Command line tool for LAVA quality assurance tasks. === Install software from upstream code === $ git clone git+ssh://git.collabora.co.uk/git/singularity/tools/lqa.git $ cd lqa/ $ sudo python setup.py install === Install software from Debian package === The lqa package is built at Collabora OBS: https://build.collabora.co.uk/package/show?package=lqa&project=collabora%3Apublic%3Atools And it is published at Collabora repositories: deb https://repositories.collabora.co.uk/obs/debian/ tools where `` can be `wheezy`, `jessie` or `precise`. === Usage example === LAVA is just an automated framework to push in jobs, then those jobs run the useful test case definitions. For Singularity OS, the test and job definitions are found at: http://cgit.collabora.com/git/singularity/qa/test-definitions.git/ git+ssh://git.collabora.co.uk/git/singularity/qa/test-definitions.git For other projects, those might reuse test definitions, but probably need a separated test jobs tree. The `examples/lqa.yaml` file is provided as an example that the developer must update before lqa usage and place it into the directory `$HOME/.config/`. Extra examples on jobs are also provided in the `examples` directory. Now for the real usage, tests can be run in a lot against certain image: $ lqa --log-file /var/log/lqa-sin-validator.log \ submit \ -g examples/profiles.yaml \ --all-profiles \ -t image_date:150305-004801 \ templates/*.json The previous example will run all jobs from `templates/*.json`, using the configuration file `$HOME/.config/lqa.yaml`, for all profiles configured in the file `examples/profiles.yaml`. The image to be used will be the one with timestamp `150305-004801` and actions will be logged at `lqa-sin-validator.log`. If the `lqa.yaml` configuration file is located in another directory, the `--config` or `-c` options can be used like: $ lqa --config examples/lqa.yaml \ --log-file /var/log/lqa-sin-validator.log \ submit \ -g examples/profiles.yaml \ --all-profiles \ -t image_date:150305-004801 \ templates/*.json The previous example will run all jobs in `templates/*.json`, using the configuration file `examples/lqa.yaml`, for all profiles configured in the file `examples/profiles.yaml`. The image to be used will be the one with timestamp `150305-004801` and actions will be logged at `lqa-sin-validator.log`. B&I/QA Team singularity@lists.collabora.co.uk lqa-20180227.0/lqa_tests/0000755000175000017500000000000013245261463015177 5ustar zumbizumbi00000000000000lqa-20180227.0/lqa_tests/test_merge_profiles.py0000644000175000017500000001433213245237772021623 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import unittest from lqa_tool.utils import merge_profiles profile_1={ 'name': 'basic_profile', 'path': '/some/path', 'variables': { 'a': 1, 'b': 2 } } profile_2={ 'name': 'merged_profile', 'variables': { 'b': 9, 'c': 3 } } main_profile={ 'name': 'main-profile', 'template-dir': 'templates/', 'templates': [ 'packages-tpl.json', 'validation-tpl.json', 'boot-tpl.json' ], 'variables': { 'imgpath': 'daily/1.0/', 'release_version': '1.0', 'stream': '/public/team/lqa/', 'bootfstype': 'ext4' } } sub_profile_1={ 'name': 'daily-i386-vm', 'variables': { 'pretty': "I386 VM image", 'device_type': 'i386', 'arch': 'i386', 'board': 'i386', 'fs': 'btrfs' } } sub_profile_2={ 'name': 'daily-minnowboard-max', 'variables': { 'pretty': "Minnowboard max image", 'device_type': 'minnowboard-max-E3825', 'arch': 'amd64', 'board': 'amd64-uefi', 'fs': 'btrfs', 'bootfstype': 'vfat' } } nested_profile={ 'name': 'profile', 'variables': { 'templates': [ 'tpl_0', 'tpl_1', 'tpl_2' ], 'sub_variables': { 'sub_templates': [ 'tpl_0', 'tpl_1' ] }, 'streams': { 'public': '/public/team/lqa/', 'private': '/private/team/lqa/', 'anonymous': '/anonymous/lqa/', 'devel': { 'anonymous': '/anonymous/devel/', 'tests': '/public/tests/lqa/' } }, 'fs': 'ext2', 'arch': 'i386' }, 'version': 1.0, 'rev': 1 } nested_sub_profile={ 'name': 'devel_profile', 'variables': { 'sub_variables': { 'sub_templates': [ 'tpl_3' ] }, 'streams': { 'private': '/private/team/lqa/lqa-tests/', 'devel': { 'anonymous': '/anonymous/lqa-devel/', 'private': '/private/team/lqa-devel/' } }, 'fs': 'ext4', }, 'version': 2.0 } class MergeProfiles(unittest.TestCase): def test_merge_profiles_basic(self): self.assertEqual( merge_profiles(profile_1, profile_2), { 'name': 'merged_profile', 'path': '/some/path', 'variables': { 'a': 1, 'b': 9, 'c': 3 } } ) def test_merge_profiles_main_with_sub_1(self): self.assertEqual( merge_profiles(main_profile, sub_profile_1), { 'name': 'daily-i386-vm', 'template-dir': 'templates/', 'templates': [ 'packages-tpl.json', 'validation-tpl.json', 'boot-tpl.json' ], 'variables': { 'pretty': "I386 VM image", 'device_type': 'i386', 'arch': 'i386', 'board': 'i386', 'fs': 'btrfs', 'imgpath': 'daily/1.0/', 'release_version': '1.0', 'stream': '/public/team/lqa/', 'bootfstype': 'ext4' } }) def test_merge_profiles_main_with_sub_2(self): self.assertEqual( merge_profiles(main_profile, sub_profile_2), { 'name': 'daily-minnowboard-max', 'template-dir': 'templates/', 'templates': [ 'packages-tpl.json', 'validation-tpl.json', 'boot-tpl.json' ], 'variables': { 'pretty': "Minnowboard max image", 'device_type': 'minnowboard-max-E3825', 'imgpath': 'daily/1.0/', 'release_version': '1.0', 'arch': 'amd64', 'board': 'amd64-uefi', 'fs': 'btrfs', 'stream': '/public/team/lqa/', 'bootfstype': 'vfat' } }) def test_merge_profiles_nested(self): self.assertEqual( merge_profiles(nested_profile, nested_sub_profile), { 'name': 'devel_profile', 'variables': { 'templates': [ 'tpl_0', 'tpl_1', 'tpl_2' ], 'sub_variables': { 'sub_templates': [ 'tpl_3' ] }, 'streams': { 'public': '/public/team/lqa/', 'private': '/private/team/lqa/lqa-tests/', 'anonymous': '/anonymous/lqa/', 'devel': { 'tests': '/public/tests/lqa/', 'anonymous': '/anonymous/lqa-devel/', 'private': '/private/team/lqa-devel/' } }, 'fs': 'ext4', 'arch': 'i386' }, 'version': 2.0, 'rev': 1 }) lqa-20180227.0/lqa_tests/test_replace_key_fields.py0000644000175000017500000001451013245237772022430 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import json import unittest from lqa_tool.commands.submit import _replaceKeyFields # Test a basic case of key replacement. json_basic="""{ "name": "basic", "params": { "sub_name": "sub_basic", "sub_option": "sub_foo" } } """ json_basic_replaced="""{ "name": "replaced", "params": { "sub_name": "sub_basic", "sub_option": "sub_bar" } } """ # Test that numeric values are properly replaced. json_num=""" { "image": "image.tar.gz", "version": 1.0, "partition": 2, "params": { "tests": { "test_0": [ "command_1", "command_2" ], "test_1": [ "command_3", "command_4" ] } } } """ json_num_replaced=""" { "image": "image.tar.gz", "version": 2.6, "partition": 3, "params": { "tests": { "test_0": [ "command_5", "command_6" ], "test_1": [ "command_3", "command_4" ] } } } """ # Test that replacing several values work well. json_multiple=""" { "image": "image.tar.gz", "params": { "time": 1000, "time": 3000 }, "params": { "time": 200 } } """ json_multiple_replaced=""" { "image": "image.tar.gz", "params": { "time": 4000, "time": 4000 }, "params": { "time": 4000 } } """ # Test a complete example for a job template. json_job=""" { "timeout": 3600, "actions": [ { "command": "deploy_image", "parameters": { "username": "{{username}}", "image": "https://images.collabora.co.uk/singularity/daily/ceti/150625-004801/singularity-ceti-i386-core-i386_150625-004801-collabora-3.0.ext4.img.gz", "login_prompt": "login:", "password": "{{password}}", "login_commands": [ "{{command}}" ], "password_prompt": "Password:" } }, { "command": "lava_test_shell", "parameters": { "timeout": 1800, "testdef_repos": [ { "git-repo": "git://git.collabora.co.uk/git/singularity/qa/test-definitions.git", "testdef": "singularity-validation/basic-commands.yaml" }, { "git-repo": "git://git.collabora.co.uk/git/singularity/qa/test-definitions.git", "testdef": "singularity-validation/package-installation.yaml" }, { "git-repo": "git://git.collabora.co.uk/git/singularity/qa/test-definitions.git", "testdef": "singularity-validation/systemd-analyze.yaml" } ] } }, { "command": "submit_results", "parameters": { "stream": "/this/stream/will/be/replaced/", "server": "https://lava.collabora.co.uk/RPC2/" } } ], "job_name": "singularity-validation", "device_type": "i386" } """ json_job_replaced=""" { "timeout": 3600, "actions": [ { "command": "deploy_image", "parameters": { "username": "collabora", "image": "https://images.collabora.co.uk/singularity/daily/ceti/150625-004801/singularity-ceti-i386-core-i386_150625-004801-collabora-3.0.ext4.img.gz", "login_prompt": "login:", "password": "collabora", "login_commands": [ "sudo su" ], "password_prompt": "Password:" } }, { "command": "lava_test_shell", "parameters": { "timeout": 1800, "testdef_repos": [ { "git-repo": "git://git.collabora.co.uk/git/singularity/qa/test-definitions.git", "testdef": "singularity-validation/basic-commands.yaml" }, { "git-repo": "git://git.collabora.co.uk/git/singularity/qa/test-definitions.git", "testdef": "singularity-validation/package-installation.yaml" }, { "git-repo": "git://git.collabora.co.uk/git/singularity/qa/test-definitions.git", "testdef": "singularity-validation/systemd-analyze.yaml" } ] } }, { "command": "submit_results", "parameters": { "stream": "/public/personal/user/", "server": "https://lava.collabora.co.uk/RPC2/" } } ], "job_name": "singularity-validation", "device_type": "i386" } """ class ReplaceKeyFields(unittest.TestCase): def test_replace_key_fields_basic(self): self.assertEqual( _replaceKeyFields(json.loads(json_basic), { 'name': 'replaced', 'sub_option': 'sub_bar' }), json.loads(json_basic_replaced)) def test_replace_key_fields_version(self): self.assertEqual( _replaceKeyFields(json.loads(json_num), { 'version': 2.6, 'partition': 3, 'test_0': [ "command_5", "command_6" ] }), json.loads(json_num_replaced)) def test_replace_key_fields_multiple(self): self.assertEqual( _replaceKeyFields(json.loads(json_multiple), { 'time': 4000 }), json.loads(json_multiple_replaced)) def test_replace_key_fields_job(self): self.assertEqual( _replaceKeyFields(json.loads(json_job), { 'username': 'collabora', 'password': 'collabora', 'login_commands': [ "sudo su" ], 'stream': "/public/personal/user/"}), json.loads(json_job_replaced)) lqa-20180227.0/lqa_tests/__init__.py0000644000175000017500000000000013245237772017304 0ustar zumbizumbi00000000000000lqa-20180227.0/lqa_tests/test_cli.py0000644000175000017500000000674213245237772017376 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import sys import unittest from cStringIO import StringIO from lqa_tool import Cli from lqa_tool.settings import settings from lqa_tool.exceptions import ProfileNotFound # Decorator required to redirect argparser stderr messages. def redirect_stderr(func): def _r_stderr(obj): oldstderr = sys.stderr try: sys.stderr = StringIO() func(obj) finally: sys.stderr.close() sys.stderr = oldstderr return _r_stderr class CliTestCase(unittest.TestCase): def setUp(self): settings.load_config(config_file='examples/lqa.yaml') self.cli = Cli() class SubmitCliTestCase(CliTestCase): def test_cli_submit_requires_no_args(self): # Test for the _not_ raising of an exception if no arg passed. try: self.cli._parse_args(["submit"]) except SystemExit: self.fail("submit command should not require args") def test_cli_submit_profile_option_requires_profile(self): parsed_args = self.cli._parse_args(["submit", "--profile", "i386"]) with self.assertRaises(ProfileNotFound): parsed_args.func(parsed_args) def test_cli_submit_all_profile_option_requires_profile(self): parsed_args = self.cli._parse_args(["submit", "--all-profile"]) with self.assertRaises(ProfileNotFound): parsed_args.func(parsed_args) class CancelCliTestCase(CliTestCase): @redirect_stderr def test_cli_cancel_requires_args(self): with self.assertRaises(SystemExit): self.cli._parse_args(["cancel"]) class ResubmitCliTestCase(CliTestCase): @redirect_stderr def test_cli_resubmit_requires_args(self): with self.assertRaises(SystemExit): self.cli._parse_args(["resubmit"]) class WaitCliTestCase(CliTestCase): @redirect_stderr def test_cli_wait_requires_args(self): with self.assertRaises(SystemExit): self.cli._parse_args(["wait"]) class StatusCliTestCase(CliTestCase): @redirect_stderr def test_cli_status_requires_args(self): with self.assertRaises(SystemExit): self.cli._parse_args(["status"]) class ReportCliTestCase(CliTestCase): @redirect_stderr def test_cli_report_requires_args(self): with self.assertRaises(SystemExit): self.cli._parse_args(["report"]) class ResultsCliTestCase(CliTestCase): @redirect_stderr def test_cli_results_requires_args(self): with self.assertRaises(SystemExit): self.cli._parse_args(["results"]) lqa-20180227.0/lqa_tool/0000755000175000017500000000000013245261463015012 5ustar zumbizumbi00000000000000lqa-20180227.0/lqa_tool/commands/0000755000175000017500000000000013245261463016613 5ustar zumbizumbi00000000000000lqa-20180227.0/lqa_tool/commands/sversion.py0000644000175000017500000000224213245237772021043 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015, 2016 Collabora Ltd # Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_tool.commands import Command class SVersionCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): print self.server.version() lqa-20180227.0/lqa_tool/commands/test.py0000644000175000017500000001404513245237772020156 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015, 2016 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import base64 import os.path from lqa_api.job import Job, JobError from lqa_api.exit_codes import APPLICATION_ERROR from lqa_tool.commands import Command from lqa_tool.settings import lqa_logger class TestCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): try: self._run(Job(self.args.job_id, self.server)) except JobError as err: lqa_logger.error("test command: job {}: {} {}" \ .format(self.args.job_id, err.code, err.msg)) exit(APPLICATION_ERROR) def _run(self, job): for test in job.tests: # Exclude any test passed with -e/--exclude if self.args.exclude and test.name in self.args.exclude: continue # Print all tests if no test specified in the command line if (not self.args.test_name) or \ test.name in self.args.test_name or \ test.uuid in self.args.test_name: print test # Show further job metadata with --info if self.args.info: print " description: {}".format(test.description) print " version: {}".format(test.version) print " format: {}".format(test.format) print " params: {}".format(test.params) print " project: {}".format(test.project) # Strip image field (if it exists) since it can contain newline if test.image: print " image: {}".format(test.image.strip()) else: print " image: {}".format(test.image) print " location: {}".format(test.location) print " branch_url: {}".format(test.branch_url) print " branch_rev: {}".format(test.branch_rev) print " branch_vcs: {}".format(test.branch_vcs) # Show installed packages (package environment) for this test if self.args.show_packages and test.packages: for pkg in test.packages: print "P: {} {}".format(pkg['name'], pkg['version']) # Save test run attachments if --attachments is true # and there are attachments available. if self.args.attachments and test.attachments: tr_attachments_path = _make_attachments_dir( self.args.attachments, "{}-{}".format(test.name, test.uuid)) for attachment in test.attachments: _save_attachment(attachment, tr_attachments_path) # Show test results if -r/--results passed and/or save test case # attachments if --attachments option passed. if self.args.results or self.args.attachments: for result in test.results: # Print results if --results is true if self.args.results: ms = ("{} {}".format(result.get('measurement', ''), result.get('units', ''))).strip() print "- {}: {} {}".format(result['test_case_id'], result['result'], (ms and '/ '+ ms) or '') # Save test case attachments if --attachments is true # and there are attachments available. tc_attachments = \ self.args.attachments and result.get('attachments', []) if tc_attachments: # Test case attachments path is the # test_run_atttachments path + test_case_id. tc_attachments_path = _make_attachments_dir( tr_attachments_path, result['test_case_id']) for attachment in tc_attachments: _save_attachment(attachment, tc_attachments_path) if self.args.attachments: lqa_logger.info("Attachments saved to: {}".format(\ tr_attachments_path)) def _make_attachments_dir(attachments_dir, test_dir): dir_pathname = os.path.join(attachments_dir, test_dir) if not os.path.exists(dir_pathname): try: os.makedirs(dir_pathname) except EnvironmentError as e: lqa_logger.error(e) exit(APPLICATION_ERROR) return dir_pathname def _save_attachment(attachment, directory): # Replace any '/' in the attachment name by '_' filename = attachment['pathname'].replace('/', '_') attachment_pathname = os.path.join(directory, filename) try: with open(attachment_pathname, 'w') as f: f.write(base64.b64decode(attachment['content'])) except EnvironmentError as e: lqa_logger.error(e) exit(APPLICATION_ERROR) lqa-20180227.0/lqa_tool/commands/wait.py0000644000175000017500000000342013245237772020136 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015, 2016 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_api.job import Job from lqa_api.waitqueue import WaitQueue from lqa_api.exit_codes import APPLICATION_ERROR from lqa_tool.commands import Command from lqa_tool.settings import lqa_logger from lqa_tool.utils import print_add_msg, print_wait_msg, \ print_remove_msg, print_timeout_msg class WaitCmd(Command): def __init__(self, args): Command.__init__(self, args) self.waitq = WaitQueue() def run(self): for job_id in self.job_ids: self.waitq.addjob(Job(job_id, self.server), print_add_msg) try: self.waitq.wait(self.args.timeout, print_wait_msg, print_remove_msg, print_timeout_msg) exit(self.waitq.exit_code) except ValueError as e: lqa_logger.error("wait timeout: {}".format(e)) exit(APPLICATION_ERROR) lqa-20180227.0/lqa_tool/commands/online.py0000644000175000017500000000252013245237772020456 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA Tool - Command to put devices into online mode. # Copyright (C) 2016 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_tool.commands import Command class OnlineCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): self.server.put_into_online_mode(self.args.HOSTNAME, self.args.REASON, self.args.skip_health_check) lqa-20180227.0/lqa_tool/commands/results.py0000644000175000017500000000311413245237772020673 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015, 2016 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_api.job import Job, JobError from lqa_api.exit_codes import APPLICATION_ERROR from lqa_tool.settings import lqa_logger from lqa_tool.commands import Command class ResultsCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): """Fetch the result for job id list""" for job_id in self.job_ids: try: job = Job(job_id, self.server) for result_line in job.results: print result_line except JobError as err: lqa_logger.error("results command: {}".format(err)) exit(APPLICATION_ERROR) lqa-20180227.0/lqa_tool/commands/submit.py0000644000175000017500000002742413245237772020507 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import re import yaml import json import os import os.path import urlparse import jinja2.exceptions import requests from xmlrpclib import Fault from jinja2 import Environment, FileSystemLoader, \ StrictUndefined, DebugUndefined from lqa_api.exit_codes import APPLICATION_ERROR from lqa_api.job import Job from lqa_api.waitqueue import WaitQueue from lqa_api.outputlog import OutputLog, OutputLogError from lqa_tool.settings import lqa_logger from lqa_tool.utils import merge_profiles from lqa_tool.commands import Command from lqa_tool.exceptions import ProfileNotFound from lqa_tool.utils import print_add_msg, print_wait_msg, \ print_remove_msg, print_timeout_msg class Profiles(object): """Initialize profiles for command objects. :param cmd: The command object to setup :param default_config_file: The default configuration file path""" def __init__(self, cmd, default_config_file): # Set yaml configuration try: with open(default_config_file) as conf_data: self.config = yaml.safe_load(conf_data) except EnvironmentError as e: lqa_logger.error(e) exit(APPLICATION_ERROR) except yaml.scanner.ScannerError as e: lqa_logger.error(e) exit(APPLICATION_ERROR) class SubmitCmd(Command): def __init__(self, args): Command.__init__(self, args) self.waitq = WaitQueue() def run(self): """Submit job id It follows the priority for profiles processing: 1) Any profile specified by '-p' 2) All profiles from 'all-profiles' 3) Or run without any profile.... """ if self.args.profile or self.args.all_profiles: # A profile file is required for using profiles if not self.args.profile_file: raise ProfileNotFound # Get the profiles from the .yaml file self.profiles = Profiles(self, self.args.profile_file) # Fetch the main profile and merge later with any sub profile main_profile = self.profiles.config.get('main-profile', {}) if self.args.profile: # If -p/--profile specified. for profile_name in self.args.profile: # Filter profiles from the profiles file matching -p. profiles = [ profile for profile in self.profiles.config.get('profiles', []) if profile['name'] == profile_name ] # Exit with failure if no profile in the filtered list. if not profiles: lqa_logger.error( "error: profile {} not found in profile file {}" .format(profile_name, self.args.profile_file)) exit(APPLICATION_ERROR) # Run with found profiles. # There can exist profiles with the same name. for profile in profiles: self._run(merge_profiles(main_profile, profile)) else: # Otherwise is --all-profiles. for profile in self.profiles.config.get('profiles', []): self._run(merge_profiles(main_profile, profile)) else: self._run() # Wait for jobs if --wait option enabled. if self.waitq.has_jobs(): try: self.waitq.wait(self.args.wait_timeout, print_wait_msg, print_remove_msg, print_timeout_msg) # Exit from here if the --wait option is passed so the # exit code from the queue is used instead. exit(self.waitq.exit_code) except ValueError as e: lqa_logger.error("wait timeout: {}".format(e)) exit(APPLICATION_ERROR) def _mangle_json(self, data, variables, job_file): # Deserialize rendered template into a json object. # This is required to apply the 'key fields' replacement. try: json_obj = json.loads(data) except ValueError as e: lqa_logger.error("json file '{}': {}".format(job_file, e)) return None # Replace key field variables. r_json_obj = _replaceKeyFields(json_obj, variables) # Set priority. # This option overrides the profile values to the 'priority' field. if self.args.priority: r_json_obj['priority'] = self.args.priority # After all variable substitutions, check if the image url exists if # --check-image-url is true. if self.args.check_image_url: self._check_image_url(r_json_obj) # Serialize json object to json string for submitting job. return json.dumps(r_json_obj, indent=2) def _run(self, profile={}): variables = profile.get('variables', {}) if self.args.template_vars: # The variables translates to a hash that we can easily use # for mapping the fields -> values in the json template for item in self.args.template_vars: k, v = item.split(':', 1) variables[k] = v # Get the job files from either the command line if available, # or from the 'templates' profile variable otherwise. job_files = [] template_dirs = [] if self.args.submit_job: for f in self.args.submit_job: job_files.append(os.path.basename(f)) template_dirs.append(os.path.dirname(f)) else: job_files = profile.get('templates', []) template_dirs.append(profile.get('template-dir', os.getcwd())) # Choose the 'undefined' variable strategy uv = (self.args.debug_vars and DebugUndefined) or StrictUndefined # Create a template environment. # Use list/set to remove any duplicate. env = Environment(loader=FileSystemLoader(list(set(template_dirs))), undefined=uv) # Process job files for job_file in job_files: try: # Get template from environment and render it. data = env.get_template(job_file).render(variables) except TypeError as e: lqa_logger.error("type error in {}: {}".format(job_file, e)) continue except jinja2.exceptions.TemplateNotFound as e: lqa_logger.error("template not found: {}".format(e)) continue except jinja2.exceptions.UndefinedError as e: lqa_logger.error("template variable not defined in {}: {}" .format(job_file, e)) continue # V1 files we replace key-value pairs not jus templated values if job_file.endswith(".json"): data = self._mangle_json(data, variables, job_file) if data == None: continue elif not job_file.endswith(".yaml"): lqa_logger.error("Job file not recognize as v1 or v2: {}" .format(job_file)) if self.args.verbose: print data if not self.args.dry_run: try: job_id = self.server.submit_job(data) # Queue jobs ids to wait for them if 'wait' option enabled. if self.args.wait_timeout: self.waitq.addjob(Job(job_id, self.server), print_add_msg) except Fault as e: lqa_logger.error("Submitting job {}: {}".format(job_file, e)) continue lqa_logger.info("Submitted job {} with id {}" \ .format(job_file, job_id)) # Show live output if '--live' option passed if self.args.live: if len(job_files) > 1: lqa_logger.info("lqa submit: --live option is not valid " "with more than 1 file (skipping)") elif self.args.wait_timeout: lqa_logger.info("lqa submit: --live option can't be used " "together with --wait option (skipping)") else: # Everything is valid to use live option, then go for it! try: OutputLog(job_id, self.server, is_live=True, logger=lqa_logger).run() except OutputLogError as e: lqa_logger.error("lqa submit: error: {}".format(e)) exit(APPLICATION_ERROR) def _check_image_url(self, json_job_obj): """This method checks for the existence of the image field url path for all the 'deploy_image' actions (if available)""" for action in json_job_obj['actions']: if action['command'] == 'deploy_image': image = action['parameters'].get('image', None) if image: # Test for local url path url = urlparse.urlparse(image) if url.scheme == 'file': if os.path.exists(url.path): return else: url_code = requests.head(image) if url_code.status_code == requests.codes.ok: return lqa_logger.error( "lqa submit error: image url does not exist: {}" .format(image)) exit(APPLICATION_ERROR) def _replaceKeyFields(json_obj, variables): """Replace key fields This method will iterate over a JSON object and will assign values to those keys (json fields) with the same name of a (profile) variable. :param json_obj: The json object to be processed :param variables: Dictionary containing variable and values. :returns: A new json object containing the replaced values for the json fields with the same name of the specified variables. """ def _replaceKey(variable, value, json_obj): if type(json_obj) == list: for i, json_obj_v in enumerate(json_obj): json_obj[i] = _replaceKey(variable, value, json_obj_v) elif type(json_obj) == dict: for k in json_obj: # Replace key value if the 'key' == 'variable' if k == variable: json_obj[k] = value # Otherwise continue processing the json fields. else: json_obj[k] = _replaceKey(variable, value, json_obj[k]) return json_obj j = json_obj for variable in variables: j = _replaceKey(variable, variables[variable], j) return j lqa-20180227.0/lqa_tool/commands/status.py0000644000175000017500000000302113245237772020512 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_api.job import Job, JobError from lqa_api.exit_codes import APPLICATION_ERROR from lqa_tool.commands import Command from lqa_tool.settings import lqa_logger class StatusCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): for job_id in self.job_ids: try: job = Job(job_id, self.server) print "{}: {}".format(job.status, job) except JobError as err: lqa_logger.error("status command: {}".format(err)) exit(APPLICATION_ERROR) lqa-20180227.0/lqa_tool/commands/whoami.py0000644000175000017500000000221213245237772020454 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_tool.commands import Command class WhoAmICmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): print self.server.whoami() lqa-20180227.0/lqa_tool/commands/job.py0000644000175000017500000000550013245237772017745 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_api.job import Job, JobError from lqa_api.exit_codes import APPLICATION_ERROR from lqa_tool.commands import Command from lqa_tool.settings import lqa_logger class JobCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): for job_id in self.job_ids: job = Job(job_id, self.server) try: print job # Show further job metadata with --info if self.args.info: print " status: {}".format(job.status) print " bundle: {}".format(job.bundle_link) if job.sha1: print " sha1: {}".format(job.sha1) print " submit time: {}".format(job.submit_time) if job.start_time: print " start time: {}".format(job.start_time) if job.end_time: print " end time: {}".format(job.end_time) # If there is an end_time, there _should_ be a start_time # so it is good to calculate duration here. print " duration: {}" \ .format(job.end_time - job.start_time) print " priority: {}".format(job.priority) print " device type: {}".format(job.device_type) print " hostname: {}".format(job.hostname) print " worker: {}".format(job.worker_host) # Skip tests if --no-tests passed if self.args.tests: for test in job.tests: print "|", test except JobError as err: lqa_logger.error("job command: job {}: {} {}" \ .format(job_id, err.code, err.msg)) exit(APPLICATION_ERROR) lqa-20180227.0/lqa_tool/commands/queue.py0000644000175000017500000000304013245237772020314 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015, 2016 Collabora Ltd. # Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_tool.commands import QueueCommand class QueueCmd(QueueCommand): def __init__(self, args): QueueCommand.__init__(self, args) def run(self): QueueCommand.run(self, _showqueue) def _showqueue(job, _, job_data, args): time_fmt = '' if args.show_time: status = job_data[2] if status == 'submitted': time_fmt = "(submitted at: {})".format(job.submit_time) elif status == 'running': time_fmt = "(running since: {})".format(job.start_time) # Print queued job print job, time_fmt lqa-20180227.0/lqa_tool/commands/__init__.py0000644000175000017500000000631013245237772020732 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015, 2016 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import re import json from datetime import datetime from lqa_api.job import Job from lqa_api.connection import Connection from lqa_api.exit_codes import APPLICATION_ERROR from lqa_tool.settings import settings, lqa_logger from lqa_api.utils import job_id_list_from_range, RangeNotationError class Command(object): """Base object for lqa commands. :param args: The parsed arguments as returned by argparse. """ def __init__(self, args): self.args = args self.settings = settings self.server = Connection(self.settings.rpc_url) # Only build the job id list if the command has the 'job_ids' argument, # otherwise just return. if args.__dict__.get('job_ids', None): try: self.job_ids = job_id_list_from_range(self.args.job_ids) except RangeNotationError as e: lqa_logger.error(e) exit(APPLICATION_ERROR) class QueueCommand(Command): """This class contains the common logic for the commands working with jobs queue: queue and cleanqueue""" def __init__(self, args): Command.__init__(self, args) def run(self, func): date = datetime.strptime(self.args.date, "%Y%m%d").date() \ if self.args.date else None names_regexp = [] for name in self.args.name: names_regexp.append(re.compile(name)) for job_data in self.server.all_jobs(): job = Job(job_data[0], self.server) # Filter options if names_regexp: if not any(map(lambda r: r.search(job.name), names_regexp)): continue if date and not job.submit_time.date() <= date: continue if self.args.device and job.device_type not in self.args.device: continue if self.args.hostname and job.hostname not in self.args.hostname: continue if self.args.worker_host and \ job.worker_host not in self.args.worker_host: continue if self.args.user and \ self.args.user != job.details['submitter_username']: continue # Call passed function after filtering. func(job, self.server, job_data, self.args) lqa-20180227.0/lqa_tool/commands/cleanqueue.py0000644000175000017500000000250213245237772021321 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015, 2016 Collabora Ltd. # Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_tool.commands import QueueCommand from lqa_tool.commands.cancel import cancel class CleanQueueCmd(QueueCommand): def __init__(self, args): QueueCommand.__init__(self, args) def run(self): QueueCommand.run(self, _cleanqueue) def _cleanqueue(job, server, *_): print "Canceling:", job cancel(job.id, server) lqa-20180227.0/lqa_tool/commands/devices.py0000644000175000017500000000266313245237772020624 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_tool.commands import Command class DevicesCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): # device hostname, device type, device state and current running job id devices = self.server.all_devices() print " Hostname \ \tDevice Type \ \tDevice State \ \tRunning Job" print ("=" * 63) for device in devices: print "-- {}".format(" \ ".join([ str(x) for x in device])) lqa-20180227.0/lqa_tool/commands/resubmit.py0000644000175000017500000000337113245237772021031 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from xmlrpclib import Fault from lqa_api.exit_codes import APPLICATION_ERROR from lqa_tool.settings import lqa_logger from lqa_tool.commands import Command class ReSubmitCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): """Resubmit job id""" rjob_id = None for job_id in self.job_ids: try: rjob_id = self.server.resubmit_job(job_id) except Fault as e: lqa_logger.error( "resubmit_job {}: {}".format(job_id, e)) except EnvironmentError as e: lqa_logger.error(e) exit(APPLICATION_ERROR) if rjob_id: lqa_logger.info("Resubmitted job id {} with new id {}" .format(job_id, rjob_id)) lqa-20180227.0/lqa_tool/commands/liststreams.py0000644000175000017500000000310313245237772021542 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_tool.commands import Command class ListStreamsCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): for stream in self.server.streams(): print "{}{}{}{}{}" \ .format(stream['pathname'], stream['name'] and ' | name: ' + stream['name'], stream['user'] and ' | user: ' + stream['user'], stream['group'] and ' | group: ' + stream['group'], str(stream['bundle_count']) and ' | bundles: ' + str(stream['bundle_count'])) lqa-20180227.0/lqa_tool/commands/maint.py0000644000175000017500000000252713245237772020311 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA Tool - Command to put devices into maintenance mode. # Copyright (C) 2016 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_tool.commands import Command class MaintCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): self.server.put_into_maintenance_mode(self.args.HOSTNAME, self.args.REASON, self.args.email) lqa-20180227.0/lqa_tool/commands/report.py0000644000175000017500000001436513245237772020517 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Collabora Ltd. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from datetime import datetime from lqa_tool.commands import Command from lqa_api.analyse import Analyse from lqa_tool.settings import lqa_logger from lqa_tool.version import __version__ from lqa_api.exit_codes import SUCCESS, OPERATION_FAILED class ReportCmd(Command): """Sub-command to generate a LAVA lqa report""" def __init__(self, args): Command.__init__(self, args) self._exit_code = SUCCESS def run(self): lqa_logger.info("Generating lqa report for jobs ID: {}" \ .format(" ".join(map(lambda x: str(x), self.job_ids)))) self._analyse = Analyse(self.job_ids, self.server) self._jobs_by_status = self._analyse.sort_jobs_by_status() self._print_report() exit(self._exit_code) def _print_report(self): print "/========= LQA Report v{} ({}) =========/\n" \ .format(__version__, _set_datetime()) if self.args.all_results: # If --all-results passed then show all results both for failed and # successful jobs. show_failed = show_success = ['fail', 'skip', 'unknown', 'pass'] else: # Otherwise only show failed results for faileds jobs by default. show_failed, show_success = ['fail'], [] # Failed jobs jobs_count = len(self._jobs_by_status['complete']['failed'] + self._jobs_by_status['incomplete']['bundles'] + self._jobs_by_status['incomplete']['no_bundles']) if jobs_count: # Exit with failed code if there is any failed job. self._exit_code = OPERATION_FAILED print "========== Failed jobs [{}] ==========\n".format(jobs_count) self._print_jobs(self._jobs_by_status['complete']['failed'], "Complete jobs with failures", show_failed) self._print_jobs(self._jobs_by_status['incomplete']['bundles'], "Incomplete jobs with bundles", show_failed) self._print_jobs(self._jobs_by_status['incomplete']['no_bundles'], "Incomplete jobs without bundles") # Pending Jobs jobs_count = len(self._jobs_by_status['pending']['submitted'] + self._jobs_by_status['pending']['running']) if jobs_count: print "========== Pending jobs [{}] ==========\n".format(jobs_count) self._print_jobs(self._jobs_by_status['pending']['submitted'], "Submitted jobs") self._print_jobs(self._jobs_by_status['pending']['running'], "Running jobs") # Canceled Jobs jobs_count = len(self._jobs_by_status['canceled']['canceled'] + self._jobs_by_status['canceled']['canceling']) if jobs_count: print "========== Canceled jobs [{}] ==========\n".format(jobs_count) self._print_jobs(self._jobs_by_status['canceled']['canceling'], "Canceling jobs") self._print_jobs(self._jobs_by_status['canceled']['canceled'], "Canceled jobs") # Successful jobs jobs_count = len(self._jobs_by_status['complete']['successful']) if jobs_count: print "========== Successful jobs [{}] ==========\n".format(jobs_count) self._print_jobs(self._jobs_by_status['complete']['successful'], "Successful jobs (no failures)", show_success) def _print_jobs(self, jobs, section_header, results_status=[]): if jobs: print "[ --- {} --- ]\n".format(section_header) for job in jobs: _print_job_header(job) if results_status: _print_tests(job.tests, results_status, self.args.limit) def _print_job_header(job): space = " " print space, job print space, len(str(job)) * "=" if job.failure_comment: print space, "Failure:", job.failure_comment.strip() print space, "Bundle Link:", job.bundle_link print def _print_tests(tests, results_status, limit): for test in tests: is_test_printed = False for status in results_status: results = { "fail": test.results_failed, "pass": test.results_passed, "skip": test.results_skipped, "unknown": test.results_unknown }.get(status, []) # Continue to next status if no results for this one. if not results: continue # Print test name only once for all its results. if not is_test_printed: print "\t", test is_test_printed = True for i, test_case in enumerate(results, 1): if i > limit: print "\t[* NOTE: {} test cases with status {}." \ " Visit bundle link for complete list of" \ " results or set a new limit with the --limit option *]"\ .format(limit, status.upper()) break print "\t {} - {}".format(test_case['result'].upper(), test_case['test_case_id']) print def _set_datetime(): return datetime.today().strftime("%a %b %d %H:%M:%S %Y") lqa-20180227.0/lqa_tool/commands/diffresults.py0000644000175000017500000001000013245237772021514 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2016 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_api.job import Job, JobError from lqa_api.exit_codes import SUCCESS, APPLICATION_ERROR, OPERATION_FAILED from lqa_tool.commands import Command from lqa_tool.settings import lqa_logger class DiffResultsCmd(Command): def __init__(self, args): Command.__init__(self, args) try: self.job1 = Job(self.args.job1_id, self.server) self.job2 = Job(self.args.job2_id, self.server) except JobError as err: lqa_logger.error("lqa diff: {} {}".format(err.code, err.msg)) exit(APPLICATION_ERROR) self._exit_code = SUCCESS def run(self): print '(1)', self.job1 print '(2)', self.job2 for test_job1 in self.job1.tests: # Include lava tests if --infra passed. if test_job1.name == 'lava' and not self.args.infra: continue # Filter job2 tests available in job1. job2tests = filter(lambda t: t.name == test_job1.name, self.job2.tests) # Show if there are missing tests in job2 from job1. if not job2tests: print "!! {}".format(test_job1.name) continue for test_job2 in job2tests: self._is_test_name_printed = False # Iterate over the tests results for both jobs and check diffs. for tc_job1 in test_job1.results: for tc_job2 in test_job2.results: self._diff_jobs(tc_job1, tc_job2, test_job1, test_job2) # Exit exit(self._exit_code) def _diff_jobs(self, tc_job1, tc_job2, test_job1, test_job2): def _print_diff(tj1, tj2, tc1, result1, tc2, result2): # Make sure to print the test name only once for all the results. if not self._is_test_name_printed: print "1:", tj1 print "2:", tj2 self._is_test_name_printed = True # Set to FAILED only once. if self._exit_code == SUCCESS: self._exit_code = OPERATION_FAILED print " 1> {}:{}".format(tc1, result1) print " 2> {}:{}".format(tc2, result2) if tc_job1['test_case_id'] == tc_job2['test_case_id']: # Only print results if there are differences. if tc_job1['result'] != tc_job2['result']: _print_diff(test_job1, test_job2, tc_job1['test_case_id'], tc_job1['result'].upper(), tc_job2['test_case_id'], tc_job2['result'].upper()) else: # If no 'results', check for measurements diffs. m1 = tc_job1.get('measurement', False) m2 = tc_job2.get('measurement', False) u1 = tc_job1.get('units', None) u2 = tc_job2.get('units', None) if (m1 and m2) and (u1 == u2) and (m1 != m2): _print_diff(test_job1, test_job2, tc_job1['test_case_id'], "{}{}".format(m1, u1), tc_job2['test_case_id'], "{}{}".format(m2, u2)) lqa-20180227.0/lqa_tool/commands/cancel.py0000644000175000017500000000334613245237772020426 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015, 2016 Collabora Ltd. # Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from xmlrpclib import Fault from lqa_api.exit_codes import APPLICATION_ERROR from lqa_tool.settings import lqa_logger from lqa_tool.commands import Command class CancelCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): """Cancel job id""" for job_id in self.job_ids: cancel(job_id, self.server) def cancel(job_id, conn): """ This method can be used by other commands requiring to cancel jobs. """ try: conn.cancel_job(job_id) lqa_logger.info("Job {} canceled".format(job_id)) except Fault as e: lqa_logger.error("lqa cancel {}: {}".format(job_id, e)) except EnvironmentError as e: lqa_logger.error(e) exit(APPLICATION_ERROR) lqa-20180227.0/lqa_tool/commands/mkstream.py0000644000175000017500000000372013245237772021020 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from xmlrpclib import Fault from lqa_api.exit_codes import APPLICATION_ERROR from lqa_tool.commands import Command from lqa_tool.settings import lqa_logger class MkStreamCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): """Make bundle stream""" # Access permissions access = self.args.private or self.args.public # User type user = self.args.team or self.args.personal if self.args.anonymous: pathname = "/anonymous/{}".format(self.args.anonymous) elif self.args.team: pathname = "/{}/team/{}".format(access, user) else: pathname = "/{}/personal/{}".format(access, user) try: p = self.server.make_stream(pathname, self.args.description) lqa_logger.info("mkstream: Bundle stream created: {}".format(p)) except Fault as e: lqa_logger.error("mkstream error: {}".format(e.faultString)) exit(APPLICATION_ERROR) lqa-20180227.0/lqa_tool/commands/output.py0000644000175000017500000000304413245237772020534 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool - Command to show the job output (log) # Copyright (C) 2015, 2016, 2017 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_api.outputlog import OutputLog, OutputLogError from lqa_api.exit_codes import APPLICATION_ERROR from lqa_tool.settings import lqa_logger from lqa_tool.commands import Command class OutputCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): try: OutputLog(self.args.job_id, self.server, self.args.live, \ self.args.file, lqa_logger).run() except OutputLogError as e: lqa_logger.error("lqa output: error: {}".format(e)) exit(APPLICATION_ERROR) lqa-20180227.0/lqa_tool/commands/jobdef.py0000644000175000017500000000274513245237772020434 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from lqa_api.job import Job, JobError from lqa_api.exit_codes import APPLICATION_ERROR from lqa_tool.commands import Command from lqa_tool.settings import lqa_logger class JobDefCmd(Command): def __init__(self, args): Command.__init__(self, args) def run(self): """Show job definition""" try: for job_id in self.job_ids: print Job(job_id, self.server).job_definition except JobError as err: lqa_logger.error("jobdef command: {}".format(err)) exit(APPLICATION_ERROR) lqa-20180227.0/lqa_tool/utils.py0000644000175000017500000000454213245237772016537 0ustar zumbizumbi00000000000000#!/usr/bin/env python ################################################################################### # Utilities for the LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import copy def merge_profiles(original, update): """Merge two hashes. This is used to merge profiles.""" def _merge_profiles(original, update): for k, v in update.iteritems(): if type(v) == dict and (k in original and type(original[k]) == dict): _merge_profiles(original[k], update[k]) else: original[k] = update[k] return original original_copy = copy.deepcopy(original) return _merge_profiles(original_copy, update) # Convenient callback functions that can be used by the wait command. def print_add_msg(q, job): """Convenient function to print message after adding job to the queue.""" print "({}) <= Job added to wait queue | {}: {}" \ .format(len(q), job.status, job) def print_wait_msg(q, job): """Convenient function to print message after polling job.""" print "({}) Waiting job | {}: {}".format(len(q), job.status, job) def print_remove_msg(q, job): """Convenient function to print message after removing job from the queue.""" print "({}) => Job done | {}: {}".format(len(q), job.status, job) def print_timeout_msg(q, timeout): """Convenient function to print message after timeout has been reached.""" for job in q: print "({}) !! Wait timeout of {}s reached for job | {}: {}" \ .format(len(q), timeout, job.status, job) lqa-20180227.0/lqa_tool/version.py0000644000175000017500000000005413245237772017056 0ustar zumbizumbi00000000000000# Version variable __version__='20180227.0' lqa-20180227.0/lqa_tool/exceptions.py0000644000175000017500000000206013245237772017551 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Collabora Ltd. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### class ProfileNotFound(BaseException): """This exception is used when a profile is required and not available""" pass lqa-20180227.0/lqa_tool/__init__.py0000644000175000017500000005121513245237772017135 0ustar zumbizumbi00000000000000#!/usr/bin/env python ################################################################################### # LAVA QA tool # Copyright (C) 2015, 2016 Collabora Ltd # Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import sys import xmlrpclib from argparse import ArgumentParser from lqa_api.waitqueue import WAIT_DEFAULT_TIMEOUT from lqa_api.exit_codes import APPLICATION_ERROR from lqa_tool.version import __version__ from lqa_tool.commands import Command from lqa_tool.exceptions import ProfileNotFound from lqa_tool.settings import settings, lqa_logger class Cli(object): """Command line interface using argparse""" def __init__(self): self.parser = ArgumentParser(description="lqa v{}".format(__version__)) # Add options common to all sub-commands self.parser.add_argument('-c', '--config', metavar='CONFIG.yaml', help="set configuration file") self.parser.add_argument('--log-file', type=str, help="set the log file") # Sub-commands subparsers = self.parser.add_subparsers() # Submit submit_parser = subparsers.add_parser('submit', help="Submit job files") submit_parser.add_argument('submit_job', nargs='*', type=str, metavar='JOB_FILE.json', help="job file to submit") submit_parser.add_argument('-g', '--profile-file', metavar='PROFILE.yaml', help="set profile file") submit_parser.add_argument('-n', '--dry-run', action='store_true', help="Dry-run, do everyting apart from " "submitting") submit_parser.add_argument('-p', '--profile', action='append', type=str, help="specify the profiles to use " "(can be given multiple times)") submit_parser.add_argument('--all-profiles', action='store_true', help="process all the available profiles") submit_parser.add_argument('-t', '--template-vars', action='append', type=str, help="set 'field:value' " "template variables/values " "(can be given multiple times") submit_parser.add_argument('-v', '--verbose', action='store_true', help="Verbose mode (e.g. print the resulting " "json)") submit_parser.add_argument('--wait', nargs='?', type=str, metavar='TIMEOUT', dest='wait_timeout', const=WAIT_DEFAULT_TIMEOUT, help="Wait for submitted jobs to complete using" " a TIMEOUT value (Default timeout of 3 hours)") submit_parser.add_argument('--debug-vars', action='store_true', help="Debug undefined template variables") submit_parser.add_argument('--priority', choices=['high', 'medium', 'low'], help="Set the job priority"), submit_parser.add_argument('--live', action='store_true', help="show job output live") submit_parser.add_argument('--check-image-url', action='store_true', help="Check that the image url exists before " "submitting job") submit_parser.set_defaults(func=submit) # Cancel cancel_parser = subparsers.add_parser('cancel', help="Cancel job id") cancel_parser.add_argument('job_ids', nargs='+', type=str, metavar='JOB_ID', help="job id to cancel") cancel_parser.set_defaults(func=cancel) # Resubmit resubmit_parser = subparsers.add_parser('resubmit', help="Resubmit job id") resubmit_parser.add_argument('job_ids', nargs='+', type=str, metavar='JOB_ID', help="job id to resubmit") resubmit_parser.set_defaults(func=resubmit) # Make stream mkstream_parser = subparsers.add_parser('mkstream', help="Create bundle stream") mkstream_parser.add_argument('description', type=str, nargs="?", default="", metavar='DESCRIPTION', help="description of the stream") # Group mutually exclusive options # Access group: public or private mkstream_access_group = mkstream_parser.add_mutually_exclusive_group() mkstream_access_group.add_argument('--public', action='store_const', const="public", default="public", help="create public stream (default)") mkstream_access_group.add_argument('--private', action='store_const', const="private", help="create private stream") # User group: anonymous, personal or team mkstream_user_group = mkstream_parser.add_mutually_exclusive_group() mkstream_user_group.add_argument('--anonymous', type=str, metavar="NAME", help="create anonymous stream (this " "option overrides access options since " "an anonymous stream is always public)") mkstream_user_group.add_argument('--personal', type=str, nargs='?', metavar="LOGGED_USER/SLUG", # $USER is replaced by the logged in user # when creating the bundle. const="$USER", help="create personal stream and when " "not argument is passed it defaults to " "the logged in username") mkstream_user_group.add_argument('--team', type=str, metavar="TEAM_NAME[/SLUG]", help="create team stream") mkstream_parser.set_defaults(func=mkstream) # Wait wait_parser = subparsers.add_parser('wait', help="Wait for job id") wait_parser.add_argument('job_ids', nargs='+', type=str, metavar='JOB_ID', help="wait for this job id") wait_parser.add_argument('--timeout', type=str, default=WAIT_DEFAULT_TIMEOUT, help="set wait timeout") wait_parser.set_defaults(func=wait) # Status status_parser = subparsers.add_parser('status', help="Show job id status") status_parser.add_argument('job_ids', nargs='+', type=str, metavar='JOB_ID', help="show the status for job id") status_parser.set_defaults(func=status) # Job Output output_parser = subparsers.add_parser('output', help="Fetch job id output log") output_parser.add_argument('job_id', type=int, metavar='JOB_ID', help="job id") output_group = output_parser.add_mutually_exclusive_group() output_group.add_argument('--file', type=str, metavar='FILE', help="write output to file") output_group.add_argument('--live', action='store_true', help="follow output log live") output_parser.set_defaults(func=output) # Job information job_parser = subparsers.add_parser('job', help="Get information for job id") job_parser.add_argument('job_ids', nargs='+', type=str, metavar='JOB_ID', help="job id from which to fetch information") job_parser.add_argument('--info', action='store_true', help="show job metadata information") job_parser.add_argument('-t', '--tests', action='store_true', help="show the tests list") job_parser.set_defaults(func=job) # Test information test_parser = subparsers.add_parser('test', help="Show test information") test_parser.add_argument('job_id', type=int, metavar='JOB_ID', help="job id") test_parser.add_argument('test_name', nargs='*', type=str, metavar='TEST_NAME', help="test name or test uuid to get results, if" " no test is specified it will list all tests") test_parser.add_argument('--info', action='store_true', help="show test metadata information") test_parser.add_argument('--show-packages', action='store_true', help="show the installed packages in the system" " for the test (it can be a long output)") test_parser.add_argument('-e', '--exclude', action='append', type=str, help="exclude test from the result " "(can be given multiple times)") test_parser.add_argument('-r', '--results', action='store_true', help="show test results") test_parser.add_argument('--attachments', type=str, metavar="DIR", help="save test attachments to DIR") test_parser.set_defaults(func=test) # Tests diff diff_parser = subparsers.add_parser('diffresults', help="Show test results differences") diff_parser.add_argument('job1_id', type=int, metavar='JOB1_ID', help="job 1 id") diff_parser.add_argument('job2_id', type=int, metavar='JOB2_ID', help="job 2 id") diff_parser.add_argument('--infra', action='store_true', help="show infra (lava) tests differences") diff_parser.set_defaults(func=diffresults) # Report report_parser = subparsers.add_parser('report', help="Generate report" " for job id's") report_parser.add_argument('job_ids', nargs='+', type=str, metavar='JOB_ID', help="job id to generate report") report_parser.add_argument('-a', '--all-results', action='store_true', help="show all results (not only failed ones)") report_parser.add_argument('-l', '--limit', type=int, metavar="LIMIT", default=500, help="set the number of test " "results to show (defaults to 500)") report_parser.set_defaults(func=report) # Results results_parser = subparsers.add_parser('results', help="Get (raw) results") results_parser.add_argument('job_ids', nargs='+', type=str, metavar='JOB_ID', help="job id to get results") results_parser.set_defaults(func=results) # Jobdef (show job definition) jobdef_parser = subparsers.add_parser('jobdef', help="Show job definition file") jobdef_parser.add_argument('job_ids', nargs='+', type=str, metavar='JOB_ID', help="show the job definition file for job id") jobdef_parser.set_defaults(func=jobdef) # Show running and submitted jobs queue_parser = subparsers.add_parser('queue', help="Show the current queue" " of running and submitted jobs") queue_parser.add_argument('-n', '--name', action='append', type=str, default=[], help="filter jobs with NAME " "(can be given multiple times)") queue_parser.add_argument('-u', '--user', type=str, help="filter jobs with user NAME") queue_parser.add_argument('-d', '--device', action='append', type=str, help="filter jobs running in DEVICE " "(can be given multiple times)") queue_parser.add_argument('-t', '--hostname', action='append', type=str, help="filter jobs running in HOSTNAME " "(can be given multiple times)") queue_parser.add_argument('-w', '--worker-host', action='append', type=str, help="filter jobs running in WORKER_HOST " "(can be given multiple times)") queue_parser.add_argument('-e', '--date', type=str, metavar="YYYYMMDD", help="show jobs with date equal or older " "than YYYYMMDD") queue_parser.add_argument('-s', '--show-time', action='store_true', help="show time in the queue for the jobs") queue_parser.set_defaults(func=queue) # Clean the job queue cleanqueue_parser = subparsers.add_parser('cleanqueue', help="Clean jobs queue") cleanqueue_parser.add_argument('-n', '--name', action='append', type=str, default=[], help="clean jobs with NAME " "(can be given multiple times)") cleanqueue_parser.add_argument('-u', '--user', type=str, help="clean jobs with user NAME") cleanqueue_parser.add_argument('-d', '--device', action='append', type=str, help="clean jobs running in DEVICE " "(can be given multiple times)") cleanqueue_parser.add_argument('-t', '--hostname', action='append', type=str, help="clean jobs running in HOSTNAME " "(can be given multiple times)") cleanqueue_parser.add_argument('-w', '--worker-host', action='append', type=str, help="clean jobs running in WORKER_HOST " "(can be given multiple times)") cleanqueue_parser.add_argument('-e', '--date', type=str, metavar="YYYYMMDD", help="clean jobs with date equal or older " "than YYYYMMDD") cleanqueue_parser.set_defaults(func=cleanqueue) # Maintenance maint_parser = subparsers.add_parser('maint', help="Put the given " "device in maintenance mode") maint_parser.add_argument('HOSTNAME', type=str, help="Name of the device") maint_parser.add_argument('REASON', type=str, help="Reason to put the " "device in maintenance mode.") maint_parser.add_argument('--email', type=str, default='', help="Email address of the user to notify " "when the job has finished") maint_parser.set_defaults(func=maint) # Online online_parser = subparsers.add_parser('online', help="Put the given " "device into online mode") online_parser.add_argument('HOSTNAME', type=str, help="Name of the device") online_parser.add_argument('REASON', type=str, help="Reason to put the " "device into online mode.") online_parser.add_argument('--skip-health-check', action='store_true', default=False, help="Skip health check") online_parser.set_defaults(func=online) # Show devices devices_parser = subparsers.add_parser('devices', help="Show status of all" " available devices") devices_parser.set_defaults(func=devices) # List streams streams_parser = subparsers.add_parser('liststreams', help="Show streams " "the user has access to") streams_parser.set_defaults(func=liststreams) # Who am I? whoami_parser = subparsers.add_parser('whoami', help="Show authenticated " "user name") whoami_parser.set_defaults(func=whoami) # Server version sversion_parser = subparsers.add_parser('sversion', help="Show LAVA server version") sversion_parser.set_defaults(func=sversion) def run(self): args = self._parse_args() # Catch any high level exception at this point try: settings.load_config(config_file=args.config, log_file=args.log_file) args.func(args) except ProfileNotFound: self.parser.error('Please specify a profile file using the -g option') except xmlrpclib.ProtocolError as e: # Catch any XMLRPC protocol error at this point. lqa_logger.error("xmlrpc error: {}".format(e)) exit(APPLICATION_ERROR) except KeyboardInterrupt: pass def _parse_args(self, args=sys.argv[1:]): return self.parser.parse_args(args) def submit(args): from lqa_tool.commands.submit import SubmitCmd SubmitCmd(args).run() def cancel(args): from lqa_tool.commands.cancel import CancelCmd CancelCmd(args).run() def resubmit(args): from lqa_tool.commands.resubmit import ReSubmitCmd ReSubmitCmd(args).run() def mkstream(args): from lqa_tool.commands.mkstream import MkStreamCmd MkStreamCmd(args).run() def wait(args): from lqa_tool.commands.wait import WaitCmd WaitCmd(args).run() def status(args): from lqa_tool.commands.status import StatusCmd StatusCmd(args).run() def output(args): from lqa_tool.commands.output import OutputCmd OutputCmd(args).run() def job(args): from lqa_tool.commands.job import JobCmd JobCmd(args).run() def test(args): from lqa_tool.commands.test import TestCmd TestCmd(args).run() def diffresults(args): from lqa_tool.commands.diffresults import DiffResultsCmd DiffResultsCmd(args).run() def report(args): from lqa_tool.commands.report import ReportCmd ReportCmd(args).run() def results(args): from lqa_tool.commands.results import ResultsCmd ResultsCmd(args).run() def jobdef(args): from lqa_tool.commands.jobdef import JobDefCmd JobDefCmd(args).run() def queue(args): from lqa_tool.commands.queue import QueueCmd QueueCmd(args).run() def cleanqueue(args): from lqa_tool.commands.cleanqueue import CleanQueueCmd CleanQueueCmd(args).run() def maint(args): from lqa_tool.commands.maint import MaintCmd MaintCmd(args).run() def online(args): from lqa_tool.commands.online import OnlineCmd OnlineCmd(args).run() def devices(args): from lqa_tool.commands.devices import DevicesCmd DevicesCmd(args).run() def liststreams(args): from lqa_tool.commands.liststreams import ListStreamsCmd ListStreamsCmd(args).run() def whoami(args): from lqa_tool.commands.whoami import WhoAmICmd WhoAmICmd(args).run() def sversion(args): from lqa_tool.commands.sversion import SVersionCmd SVersionCmd(args).run() lqa-20180227.0/lqa_tool/settings.py0000644000175000017500000001324413245237772017236 0ustar zumbizumbi00000000000000################################################################################### # LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### import os import yaml import logging import os.path from urlparse import urlsplit, urlunsplit from lqa_api.exit_codes import APPLICATION_ERROR CONFIG_PATH=os.getenv("HOME") + '/.config/' LAVA_CONFIG='lqa.yaml' lqa_logger = logging.getLogger('lqa') class Settings(object): """Initialize settings for command objects. :param cmd: The command object to setup :param default_config_file: The default configuration file path""" def __init__(self): pass def load_config(self, config_file=None, log_file=None): # Set looger # Log to file using formatter if --log-file passed, otherwise to stderr if log_file: hdl = logging.FileHandler(filename=log_file) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s ' '- %(message)s') hdl.setFormatter(formatter) else: hdl = logging.StreamHandler() lqa_logger.addHandler(hdl) lqa_logger.setLevel(logging.DEBUG) # Check for the configuration file and retrieve values self.config_file = self.find_config_file(config_file) # Set yaml configuration try: with open(self.config_file) as conf_data: self.config = yaml.safe_load(conf_data) except EnvironmentError as e: lqa_logger.error(e) exit(APPLICATION_ERROR) except yaml.scanner.ScannerError as e: lqa_logger.error(e) exit(APPLICATION_ERROR) # After loading the configuration file, check the minimum required values # and set the rpc_url. self.check_config_fields(['user', 'auth-token', 'server']) self.rpc_url = self._build_rpc_api_url() def find_config_file(self, config_file = None): """Find the configuration file. It follows the priority: 1) config file passed e.g. via -c/--config to the cli 2) LQA_CONFIG_FILE environment variable value if it is set 3) local 'CONF.yaml' file 4) global CONF.yaml file from $HOME/.config/ :param config: Optional override of the default :returns: The found and available configuration file in the system """ default_config_path = os.path.join(CONFIG_PATH, LAVA_CONFIG) lqa_config_file = os.getenv("LQA_CONFIG_FILE") if config_file: if not os.path.isfile(config_file): lqa_logger.error("Configuration file '{}' not found" .format(config_file)) exit(APPLICATION_ERROR) elif lqa_config_file: config_file = lqa_config_file elif os.path.isfile(os.path.join(os.getcwd(), LAVA_CONFIG)): config_file = os.path.join(os.getcwd(), LAVA_CONFIG) elif os.path.isfile(default_config_path): config_file = default_config_path else: # No config file?.. exit lqa_logger.error("Configuration file '{}' not found. Or use the " "-c/--config flag to specify a different file" .format(default_config_path)) exit(APPLICATION_ERROR) return config_file def check_config_fields(self, config_fields): """Check that the configuration file has the required fields to proceed""" # Make sure config exists since it can be None if no yaml # fields are defined. if self.config: for field in config_fields: if field not in self.config: lqa_logger.error("Required field '{}' not found in " "configuration file {}" .format(field, self.config_file)) exit(APPLICATION_ERROR) else: lqa_logger.error("No configuration fields available in file {}" .format(self.config_file)) exit(APPLICATION_ERROR) def _build_rpc_api_url(self): (scheme, netloc, _, _, _) = urlsplit(self.config['server']) if not scheme or not netloc: lqa_logger.error("The syntax '{}' for server field is not valid at {}" .format(self.config['server'], self.config_file)) lqa_logger.error("Please use the form '[scheme]://[address]/' for the " "server field") exit(APPLICATION_ERROR) return urlunsplit((scheme, "{}:{}@{}".format(self.config['user'], self.config['auth-token'], netloc), 'RPC2', '', '')) settings = Settings() lqa-20180227.0/setup.py0000644000175000017500000000272613245237772014727 0ustar zumbizumbi00000000000000#!/usr/bin/env python ################################################################################### # LAVA QA tool # Copyright (C) 2015 Luis Araujo # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 US ################################################################################### from setuptools import setup, find_packages execfile('lqa_tool/version.py') setup( name='lqa', version=__version__, license='LGPL-2.1+', description='LAVA QA command line tool', author='Luis Araujo', author_email='luis.araujo@collabora.co.uk', url='http://cgit.collabora.com/git/singularity/tools/lqa.git/', packages=find_packages(), scripts=['lqa'], include_package_data=False, install_requires=['PyYaml', 'jinja2', 'requests'], zip_safe=False, )