#!/usr/bin/env python3 """ Copyright (c) 2015, Intel Corporation Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Intel Corporation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ from subprocess import call,check_output from pprint import pprint from numpy import * import subprocess import argparse import shutil import sys import os import requests import datetime ezbench_dir = os.path.dirname(os.path.realpath(__file__)) sys.path.append(os.path.join(ezbench_dir, 'python-modules')) from ezbench.smartezbench import * def break_lists(input_list, sep=None): res = [] if input_list is None: return res for entry in input_list: res.extend(entry.split(sep)) return res # parse the options attributes_val = dict() def AttributeString(v): fields = v.split("=") if len(fields) != 2: raise argparse.ArgumentTypeError("Attributes need the format $name=(float)") else: attributes_val[fields[0]] = fields[1] return fields[0] parser = argparse.ArgumentParser() parser.add_argument("-b", dest='tests', help=" include these tests to run", action="append") parser.add_argument("-B", dest='tests_exclude', help=" exclude these benchmarks from running", action="append") parser.add_argument("-t", dest='testsets', help="Test sets to run", action="append") parser.add_argument("-T", dest='list_testsets', help="List the available testsets", action="store_true") parser.add_argument("-c", dest='commits', help="Commits to run the tests on", action="append") parser.add_argument("-r", dest='rounds', help="Number of execution rounds to add", action="store", type=int, nargs='?') parser.add_argument("-e", dest='ensure', help="Make sure that at least N rounds are executed", action="store", type=int, nargs='?') parser.add_argument("-p", dest='profile', help="Profile to be used by ezbench", action="store") parser.add_argument("-s", dest='add_conf_script', help="Add a configuration script for EzBench's runner", action="append") parser.add_argument("-S", dest='remove_conf_script', help="Remove a configuration script for EzBench's runner", action="append") parser.add_argument("-a", dest='attributes', help="Set an attribute", type=AttributeString, action="append", choices=SmartEzbench.attributes()) parser.add_argument("-l", dest='list_tests', help="List the available benchmarks", action="store_true") parser.add_argument("-R", dest='rest_server', help="Base URL of controllerd REST server. If given, passes commands to the REST server instead.", action="store") parser.add_argument("-m", dest='machines', help="DUT name to operate on (repeatable). If not given, operate on all known machines.", action="append") parser.add_argument("-M", dest='list_machines', help="List available DUTs", action="store_true") parser.add_argument("report_name", nargs='?') parser.add_argument("command", help="Command to execute", nargs='?', choices=('start', 'run', 'pause', 'abort', 'status', 'delete')) args = parser.parse_args() # TODO: Add a way to list jobs, and a way to download/fetch the reports class CommandHandler: pass class RestHandler(CommandHandler): def __init__(self, server): self.server = server self.machines = [] self.attributes = {} def get(self, url): fullurl = self.server + url r = requests.get(fullurl) if r.status_code == 200: return r.json() else: return None def __check_return__(self, ret): if ret.status_code != 200: print(ret.text) return False return True def post(self, url, data): fullurl = self.server + url self.__check_return__(requests.post(fullurl, json=data)) def put(self, url, data): fullurl = self.server + url self.__check_return__(requests.put(fullurl, json=data)) def patch(self, url, data): fullurl = self.server + url self.__check_return__(requests.patch(fullurl, json=data)) def delete(self, url): fullurl = self.server + url self.__check_return__(requests.delete(fullurl)) def add_machines(self, machines): for m in machines: self.machines.append(m) def add_all_machines(self): state = self.get("/machines") for machine in state['machines']: self.machines.append(machine) def available_tests(self): tests = set() state = self.get("/machines") for machine in self.machines: mstate = self.get(state['machines'][machine]['url']) for test in mstate['tests']: tests.add(test) return tests def available_testsets(self): testsets = set() state = self.get("/machines") for machine in self.machines: mstate = self.get(state['machines'][machine]['url']) for testset in mstate['testsets']: testsets.add(testset) return testsets def set_report(self, report_name, profile): # Create the job if it doesn't exist and we have a profile to give self.job_url = "/jobs/{}".format(report_name) state = self.get(self.job_url) if state is None and profile is not None: payload = { "description": "Test job", "profile": profile, "attributes": self.attributes, "machines": [] } for m in self.machines: payload["machines"].append(m) self.post(self.job_url, payload) self.job_name = report_name self.profile_name = profile def set_attribute(self, key, val): self.attributes[key] = val def profile(self): return self.profile_name def add_tests(self, commits, tests, tests_exclude, rounds): # TODO: tests_exclude work = { "commits": {} } for commit in commits: work["commits"][commit] = { "tests": {} } for test in tests: work["commits"][commit]["tests"][test] = rounds self.patch(self.job_url + "/work", work) def run(self): print("Invalid command: REST interface automatically runs all queued jobs") sys.exit(1) def set_running_mode(self, mode): print("Invalid command: REST interface automatically runs all queued jobs") sys.exit(1) def status(self): state = self.get(self.job_url) if state is None: print("Invalid job") sys.exit(1) all_done = True print("Job: {}".format(state['id'])) print("Description: {}".format(state['description'])) print("Profile: {}".format(state['profile'])) print("Attributes:") for attr in state['attributes']: print(" {}: {}".format(attr, state['attributes'][attr])) print("Machines:") for name in state['machines']: print(" {}:".format(name)) if state['machines'][name]['online']: onlinestatus = "yes" else: mstatus = self.get("/machines/{}".format(name)) stamp = datetime.fromtimestamp(mstatus['last_seen']) onlinestatus = "no (last seen: {})".format(stamp.isoformat(' ')) print(" Online: {}".format(onlinestatus)) if state['machines'][name]['state'] == 'MISSING': print(" Does not have this job yet") all_done = False continue mreport = self.get(state['machines'][name]['report']) print(" State: {} (on disk: {})".format(mreport['state'], mreport['state_disk'])) if mreport['state'] != 'DONE': all_done = False print(" Average deploy time: {}".format(mreport['deploy_time'])) print(" Average build time: {}".format(mreport['build_time'])) if all_done: print("All machines DONE") def delete_job(self): self.delete(self.job_url) def list_machines(self): machines = self.get("/machines") for m in machines['machines']: url = machines['machines'][m]['url'] mch = self.get(url) print("{}:".format(m)) if mch['online']: onlinestatus = "yes" else: stamp = datetime.fromtimestamp(mch['last_seen']) onlinestatus = "no (last seen: {})".format(stamp.isoformat(' ')) print(" Online: {}".format(onlinestatus)) print(" Reports:") isidle = True for r in mch['reports']: repstate = mch['reports'][r]['state'] print(" {}: {}".format(r, repstate)) if repstate == 'RUNNING' or repstate == 'RUN': isidle = False unsent = [] for cmd in mch['queued_cmds']: if cmd['err_code'] != 'OK': unsent.append(cmd['description']) if unsent: print(" Pending commands:") for msg in unsent: print(" {}".format(msg)) print(" Idle: {}".format(isidle)) class LocalHandler(CommandHandler): def __init__(self): self.ezbench = Ezbench(ezbench_dir = ezbench_dir) def available_tests(self): return self.ezbench.available_tests() def available_testsets(self): return Testset.list(ezbench_dir) def set_report(self, report_name, profile): self.sbench = SmartEzbench(ezbench_dir, report_name) if self.sbench.profile() is None and profile is not None: self.sbench.set_profile(profile) def add_conf_scripts(self, addlist): for add in addlist: self.sbench.add_conf_script(add) def remove_conf_scripts(self, rmlist): for rm in rmlist: self.sbench.remove_conf_script(rm) def set_attribute(self, key, val): self.sbench.set_attribute(key, val) def profile(self): return self.sbench.profile() def add_tests(self, commits, tests, tests_exclude, rounds): # get the list of tests that actually need to be ran ezb = Ezbench(ezbench_dir=ezbench_dir, profile=self.sbench.profile(), report_name="tmp") run_info = ezb.run(commits, tests, tests_exclude, dry_run=True) if not run_info.success(): sys.exit(1) # Add all the commits and tests to commit for commit in run_info.commits: for test in run_info.tests: if args.ensure is None: total_rounds = self.sbench.add_test(commit, test, rounds) if rounds >= 0: print("added {} runs to {} on {} --> {} runs".format(rounds, test, commit, total_rounds)) else: print("removed {} runs to {} on {} --> {} runs".format(-rounds, test, commit, total_rounds)) else: added = self.sbench.force_test_rounds(commit, test, int(args.ensure)) print("ensured {} runs of {} on {} --> added {} runs".format(int(args.ensure), test, commit, added)) def add_testsets(self, commits, testsets_to_be_added, rounds, ensure): # get the list of tests that actually need to be ran ezb = Ezbench(ezbench_dir=ezbench_dir, profile=self.sbench.profile(), report_name="tmp") run_info = ezb.run(commits, [], dry_run=True) if not run_info.success(): sys.exit(1) # Add the testsets specified for commit in run_info.commits: for testset in testsets_to_be_added: self.sbench.add_testset(commit, testset, rounds, ensure) def run(self): self.sbench.run() def set_running_mode(self, mode): self.sbench.set_running_mode(mode) def status(self): print("Report name:", self.sbench.report_name) print("Mode:", self.sbench.running_mode().name) print("\nAttributes:") for a in self.sbench.attributes(): print(" - {}: {}".format(a, self.sbench.attribute(a))) print("\nRaw status:") pprint.pprint(self.sbench.state) def list_machines(self): print("Not supported by the local handler", file=sys.stderr) def delete_job(self): self.sbench.delete() if args.rest_server: handler = RestHandler(args.rest_server) if args.machines is not None: handler.add_machines(args.machines) else: handler.add_all_machines() else: handler = LocalHandler() if args.list_tests: tests = handler.available_tests() for test in sorted(tests): print(test) sys.exit(0) if args.list_testsets: testsets = handler.available_testsets() if len(testsets) > 0: print("Available test sets:") for testset in testsets: if testset.parse(None, silent=False): print(" * {}\n\t{}".format(testset.name, testset.description)) else: print(" * {}: invalid because of one or more errors".format(testset.name)) else: print("No test sets are available") sys.exit(0) if args.list_machines: handler.list_machines() sys.exit(0) testsets_to_be_added = [] if args.testsets is not None: # remove duplicates in the lists testsets = list(set(break_lists(args.testsets))) tests = handler.available_tests() # Check all the testsets for name in testsets: testset = Testset.open(ezbench_dir, name) if testset is None: print("Cannot find a test set named '{}'".format(name)) sys.exit(1) if not testset.parse(tests): print("Invalid test set named {}, abort...".format(name)) sys.exit(1) if args.report_name is None: print("The test set '{}' contains the following tests:".format(name)) for test in sorted(testset.keys()): print("\t{} --> {} rounds".format(test, testset[test])) print("") sys.exit(0) testsets_to_be_added.append(testset) if args.report_name is None: print("Error: The report name is missing") sys.exit(1) handler.set_report(args.report_name, args.profile) if args.add_conf_script is not None: handler.add_conf_scripts(args.add_conf_script) if args.remove_conf_script is not None: handler.remove_conf_scripts(args.remove_conf_script) if args.attributes is not None: for attr in set(args.attributes): handler.set_attribute(attr, float(attributes_val[attr])) # add commits and tests if args.commits is not None and args.tests is not None: # remove duplicates in the lists commits = list(set(break_lists(args.commits))) tests = list(set(break_lists(args.tests))) tests_exclude = list(set(break_lists(args.tests_exclude))) # we cannot fetch the git sha1 without a profile/git repo if handler.profile() is None: print("No profile is set, set one first with -p before adding test runs") sys.exit(1) # Default to 3 round if -r is not set if args.rounds is None: rounds = 3 else: rounds = int(args.rounds) handler.add_tests(commits, tests, tests_exclude, rounds) if args.commits is not None and len(testsets_to_be_added) > 0: # remove duplicates in the lists commits = list(set(break_lists(args.commits))) # we cannot fetch the git sha1 without a profile/git repo if handler.profile() is None: print("No profile is set, set one first with -p before adding test runs") sys.exit(1) # Ensure runs if set if args.ensure is None: # Default to 1 round if -r is not set if args.rounds is None: rounds = 1 else: rounds = int(args.rounds) ensure = False else: rounds = int(args.ensure) ensure = True handler.add_testsets(commits, testsets_to_be_added, rounds, ensure) if args.command is not None: if args.command == "start": handler.run() elif args.command == "run": handler.set_running_mode(RunningMode.RUN) elif args.command == "pause": handler.set_running_mode(RunningMode.PAUSE) elif args.command == "abort": handler.set_running_mode(RunningMode.ABORT) elif args.command == "status": handler.status() elif args.command == "delete": handler.delete_job() else: print("Unknown command '{cmd}'".format(cmd=args.command))