lavacli-0.7/0000755000175000017500000000000013241511244014526 5ustar stylesenstylesen00000000000000lavacli-0.7/lavacli/0000755000175000017500000000000013241511244016141 5ustar stylesenstylesen00000000000000lavacli-0.7/lavacli/utils.py0000644000175000017500000000161013226116343017655 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see def print_u(string): try: print(string) except UnicodeEncodeError: print(string.encode("ascii", errors="replace").decode("ascii")) lavacli-0.7/lavacli/__main__.py0000644000175000017500000000155413226116343020244 0ustar stylesenstylesen00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see import sys from . import main if __name__ == '__main__': sys.argv[0] = "lavacli" sys.exit(main()) lavacli-0.7/lavacli/__about__.py0000644000175000017500000000200413236771255020433 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see __all__ = ["__author__", "__description__", "__license__", "__url__", "__version__"] __author__ = "Rémi Duraffort" __description__ = 'LAVA XML-RPC command line interface' __license__ = 'AGPLv3+' __url__ = 'https://git.linaro.org/lava/lavacli.git' __version__ = "0.7" lavacli-0.7/lavacli/commands/0000755000175000017500000000000013241511244017742 5ustar stylesenstylesen00000000000000lavacli-0.7/lavacli/commands/identities.py0000644000175000017500000000674713241511171022472 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2018 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see import os import yaml def configure_parser(parser): sub = parser.add_subparsers(dest="sub_sub_command", help="Sub commands") sub.required = True # "add" config_add = sub.add_parser("add", help="add an identity") config_add.add_argument("id", type=str, help="identity") config_add.add_argument("--uri", type=str, required=True, help="URI of the lava-server RPC endpoint") config_add.add_argument("--proxy", type=str, default="", help="http proxy") # "delete" config_del = sub.add_parser("delete", help="delete an alias") config_del.add_argument("id", help="identity") # "list" config_list = sub.add_parser("list", help="list available identities") # "show" config_show = sub.add_parser("show", help="show identity details") config_show.add_argument("id", type=str, help="identity") def help_string(): return "manage lavacli configuration" def _load_configuration(): config_dir = os.environ.get("XDG_CONFIG_HOME", "~/.config") config_filename = os.path.expanduser(os.path.join(config_dir, "lavacli.yaml")) try: with open(config_filename, "r", encoding="utf-8") as f_conf: return yaml.load(f_conf.read()) except (FileNotFoundError, KeyError, TypeError): return None def _save_configuration(config): config_dir = os.environ.get("XDG_CONFIG_HOME", "~/.config") config_filename = os.path.expanduser(os.path.join(config_dir, "lavacli.yaml")) with open(config_filename, "w", encoding="utf-8") as f_conf: f_conf.write(yaml.dump(config, default_flow_style=False).rstrip("\n")) def handle_add(proxy, options): config = _load_configuration() config[options.id] = {"uri": options.uri} if options.proxy: config[options.id]["proxy"] = options.proxy _save_configuration(config) def handle_delete(proxy, options): config = _load_configuration() del config[options.id] _save_configuration(config) def handle_list(proxy, option): config = _load_configuration() print("Identities:") for identity in sorted(config.keys()): print("* %s" % identity) def handle_show(proxy, options): config = _load_configuration() try: conf_str = yaml.dump(config[options.id], default_flow_style=False) print(conf_str.rstrip('\n')) except KeyError: print("Unknown identity '%s'" % options.id) return 1 def handle(proxy, options, _): handlers = { "add": handle_add, "delete": handle_delete, "list": handle_list, "show": handle_show } return handlers[options.sub_sub_command](proxy, options) lavacli-0.7/lavacli/commands/results.py0000644000175000017500000000562713226116343022033 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see import sys import yaml def configure_parser(parser): parser.add_argument("job_id", help="job id") parser.add_argument("test_suite", nargs="?", default=None, help="test suite") parser.add_argument("test_case", nargs="?", default=None, help="test case") parser.add_argument("--yaml", dest="output_format", default=None, action="store_const", const="yaml", help="print as yaml") def help_string(): return "manage results" def handle(proxy, options, _): if options.test_case is not None: data = proxy.results.get_testcase_results_yaml(options.job_id, options.test_suite, options.test_case) elif options.test_suite is not None: data = proxy.results.get_testsuite_results_yaml(options.job_id, options.test_suite) else: data = proxy.results.get_testjob_results_yaml(options.job_id) results = yaml.load(data) if options.output_format == "yaml": print(yaml.dump(results).rstrip("\n")) else: # Only one to print if options.test_case is not None: res = results[0] if not sys.stdout.isatty(): print("%s" % res["result"]) elif res["result"] == "pass": print("\033[1;32mpass\033[0m") elif res["result"] == "fail": print("\033[1;31mfail\033[0m") else: print("%s" % res["result"]) # A list to print else: print("Results:") for res in results: if not sys.stdout.isatty(): print("* %s.%s [%s]" % (res["suite"], res["name"], res["result"])) elif res["result"] == "pass": print("* %s.%s [\033[1;32mpass\033[0m]" % (res["suite"], res["name"])) elif res["result"] == "fail": print("* %s.%s [\033[1;31mfail\033[0m]" % (res["suite"], res["name"])) else: print("* %s.%s [%s]" % (res["suite"], res["name"], res["result"])) lavacli-0.7/lavacli/commands/workers.py0000644000175000017500000001544713226116343022027 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see import argparse import time import yaml def configure_parser(parser): sub = parser.add_subparsers(dest="sub_sub_command", help="Sub commands") sub.required = True # "add" workers_add = sub.add_parser("add", help="add a worker") workers_add.add_argument("hostname", type=str, help="worker hostname") workers_add.add_argument("--description", type=str, default=None, help="worker description") workers_add.add_argument("--disabled", action="store_true", default=False, help="create a disabled worker") # "config" workers_config = sub.add_parser("config", help="worker configuration") config_sub = workers_config.add_subparsers(dest="sub_sub_sub_command", help="Sub commands") config_sub.required = True config_get = config_sub.add_parser("get", help="get the worker configuration") config_get.add_argument("hostname", type=str, help="worker hostname") config_set = config_sub.add_parser("set", help="set the worker configuration") config_set.add_argument("hostname", type=str, help="worker hostname") config_set.add_argument("config", type=argparse.FileType('r'), help="configuration file") # "list" workers_list = sub.add_parser("list", help="list workers") workers_list.add_argument("--yaml", dest="output_format", action="store_const", const="yaml", default=None, help="print as yaml") # "maintenance" workers_maintenance = sub.add_parser("maintenance", help="maintenance the worker") workers_maintenance.add_argument("hostname", type=str, help="worker hostname") workers_maintenance.add_argument("--force", default=False, action="store_true", help="force worker maintenance by canceling running jobs") workers_maintenance.add_argument("--no-wait", dest="wait", default=True, action="store_false", help="do not wait for the devices to be idle") # "update" update_parser = sub.add_parser("update", help="update worker properties") update_parser.add_argument("hostname", type=str, help="worker hostname") update_parser.add_argument("--description", type=str, default=None, help="worker description") update_parser.add_argument("--health", type=str, default=None, choices=["ACTIVE", "MAINTENANCE", "RETIRED"], help="worker health") # "show" workers_show = sub.add_parser("show", help="show worker details") workers_show.add_argument("hostname", help="worker hostname") workers_show.add_argument("--yaml", dest="output_format", action="store_const", const="yaml", default=None, help="print as yaml") def help_string(): return "manage workers" def handle_add(proxy, options): proxy.scheduler.workers.add(options.hostname, options.description, options.disabled) def handle_config(proxy, options): if options.sub_sub_sub_command == "get": config = proxy.scheduler.workers.get_config(options.hostname) print(str(config).rstrip("\n")) else: config = options.config.read() ret = proxy.scheduler.workers.set_config(options.hostname, config) if not ret: print("Unable to store worker configuration") return 1 def handle_list(proxy, options): workers = proxy.scheduler.workers.list() if options.output_format == "yaml": print(yaml.dump(workers).rstrip("\n")) else: print("Workers:") for worker in workers: print("* %s" % worker) def handle_maintenance(proxy, options): proxy.scheduler.workers.update(options.hostname, None, "MAINTENANCE") if options.force or options.wait: worker_devices = proxy.scheduler.workers.show(options.hostname)["devices"] for device in proxy.scheduler.devices.list(): if device["hostname"] not in worker_devices: continue current_job = device["current_job"] if current_job is not None: print("-> waiting for job %s" % current_job) # if --force is passed, cancel the job if options.force: print("--> canceling") proxy.scheduler.jobs.cancel(current_job) while options.wait and proxy.scheduler.jobs.show(current_job)["state"] != "Finished": print("--> waiting") time.sleep(5) def handle_show(proxy, options): worker = proxy.scheduler.workers.show(options.hostname) if options.output_format == "yaml": print(yaml.dump(worker).rstrip("\n")) else: print("hostname : %s" % worker["hostname"]) print("description : %s" % worker["description"]) print("state : %s" % worker["state"]) print("health : %s" % worker["health"]) print("devices : %s" % ", ".join(worker["devices"])) print("last ping : %s" % worker["last_ping"]) def handle_update(proxy, options): proxy.scheduler.workers.update(options.hostname, options.description, options.health) def handle(proxy, options, _): handlers = { "add": handle_add, "config": handle_config, "list": handle_list, "maintenance": handle_maintenance, "show": handle_show, "update": handle_update } return handlers[options.sub_sub_command](proxy, options) lavacli-0.7/lavacli/commands/utils.py0000644000175000017500000000411113226116343021455 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see import argparse import yaml from . import jobs def configure_parser(parser): sub = parser.add_subparsers(dest="sub_sub_command", help="Sub commands") sub.required = True # "logs" logs = sub.add_parser("logs", help="log helpers") logs_sub = logs.add_subparsers(dest="sub_sub_sub_command", help="Sub commands") logs_sub.required = True # "logs.print" logs_print = logs_sub.add_parser("print", help="print log file") logs_print.add_argument("filename", type=argparse.FileType('r'), help="log file") logs_print.add_argument("--filters", default=None, type=str, help="comma seperated list of levels to show") logs_print.add_argument("--raw", default=False, action="store_true", help="print raw logs") def help_string(): return "utility functions" def handle_logs(proxy, options): try: logs = yaml.load(options.filename.read(), Loader=yaml.CLoader) except yaml.YAMLError: print("Invalid yaml file") else: jobs.print_logs(logs, options.raw, options.filters) def handle(proxy, options, _): handlers = { "logs": handle_logs, } return handlers[options.sub_sub_command](proxy, options) lavacli-0.7/lavacli/commands/jobs.py0000644000175000017500000003015713236771255021275 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see import argparse import datetime import sys import time import yaml from lavacli.utils import print_u def configure_parser(parser): sub = parser.add_subparsers(dest="sub_sub_command", help="Sub commands") sub.required = True # "cancel" jobs_cancel = sub.add_parser("cancel", help="cancel a job") jobs_cancel.add_argument("job_id", help="job id") jobs_definition = sub.add_parser("definition", help="job definition") jobs_definition.add_argument("job_id", help="job id") # "list" jobs_list = sub.add_parser("list", help="list jobs") jobs_list.add_argument("--start", type=int, default=0, help="skip the N first jobs") jobs_list.add_argument("--limit", type=int, default=10, help="limit to N jobs") jobs_list.add_argument("--yaml", dest="output_format", default=None, action="store_const", const="yaml", help="print as yaml") # "logs" jobs_logs = sub.add_parser("logs", help="get logs") jobs_logs.add_argument("job_id", help="job id") jobs_logs.add_argument("--no-follow", default=False, action="store_true", help="do not keep polling until the end of the job") jobs_logs.add_argument("--filters", default=None, type=str, help="comma seperated list of levels to show") jobs_logs.add_argument("--polling", default=5, type=int, help="polling interval in seconds, 5s by default") jobs_logs.add_argument("--raw", default=False, action="store_true", help="print raw logs") # "run" jobs_run = sub.add_parser("run", help="run the job") jobs_run.add_argument("definition", type=argparse.FileType('r'), help="job definition") jobs_run.add_argument("--filters", default=None, type=str, help="comma seperated list of levels to show") jobs_run.add_argument("--no-follow", default=False, action="store_true", help="do not keep polling until the end of the job") jobs_run.add_argument("--polling", default=5, type=int, help="polling interval in seconds, 5s by default") jobs_run.add_argument("--raw", default=False, action="store_true", help="print raw logs") # "show" jobs_show = sub.add_parser("show", help="job details") jobs_show.add_argument("job_id", help="job id") jobs_show.add_argument("--yaml", dest="output_format", action="store_const", const="yaml", default=None, help="print as yaml") # "resubmit" jobs_resubmit = sub.add_parser("resubmit", help="resubmit a job") jobs_resubmit.add_argument("job_id", help="job id") jobs_resubmit.add_argument("--filters", default=None, type=str, help="comma seperated list of levels to show") jobs_resubmit.add_argument("--follow", default=True, dest="no_follow", action="store_false", help="resubmit and poll for the logs") jobs_resubmit.add_argument("--polling", default=5, type=int, help="polling interval in seconds, 5s by default") jobs_resubmit.add_argument("--raw", default=False, action="store_true", help="print raw logs") # "submit" jobs_submit = sub.add_parser("submit", help="submit a new job") jobs_submit.add_argument("definition", type=argparse.FileType('r'), help="job definition") # "wait" jobs_wait = sub.add_parser("wait", help="wait for the job to finish") jobs_wait.add_argument("job_id", help="job id") jobs_wait.add_argument("--polling", default=5, type=int, help="polling interval in seconds, 5s by default") def help_string(): return "manage jobs" def handle_cancel(proxy, options): proxy.scheduler.jobs.cancel(options.job_id) def handle_definition(proxy, options): print(proxy.scheduler.jobs.definition(options.job_id)) def handle_list(proxy, options): jobs = proxy.scheduler.jobs.list(options.start, options.limit) if options.output_format == "yaml": print(yaml.dump(jobs).rstrip("\n")) else: print("Jobs:") for job in jobs: print("* %s: %s,%s [%s] (%s)" % (job["id"], job["state"], job["health"], job["submitter"], job["description"])) if sys.stdout.isatty(): COLORS = {"exception": "\033[1;31m", "error": "\033[1;31m", "warning": "\033[1;33m", "info": "\033[1;37m", "debug": "", "target": "\033[32m", "input": "\033[0;35m", "feedback": "\033[0;33m", "results": "\033[1;34m", "dt": "\033[1;30m", "end": "\033[0m"} else: COLORS = {"exception": "", "error": "", "warning": "", "info": "", "debug": "", "target": "", "input": "", "feedback": "", "results": "", "dt": "", "end": ""} def print_logs(logs, raw, filters): filters = [] if filters is None else filters.split(',') if raw: for line in logs: if filters and not line["lvl"] in filters: continue print_u("- " + yaml.dump(line, default_flow_style=True, default_style='"', width=10 ** 6, Dumper=yaml.CDumper)[:-1]) else: for line in logs: timestamp = line["dt"].split(".")[0] level = line["lvl"] if filters and level not in filters: continue if isinstance(line["msg"], dict) and \ "sending" in line["msg"].keys(): level = "input" msg = str(line["msg"]["sending"]) elif isinstance(line["msg"], bytes): msg = line["msg"].decode("utf-8", errors="replace") else: msg = str(line["msg"]) msg = msg.rstrip("\n") print_u(COLORS["dt"] + timestamp + COLORS["end"] + " " + COLORS[level] + msg + COLORS["end"]) def handle_logs(proxy, options): # Loop lines = 0 while True: (finished, data) = proxy.scheduler.jobs.logs(options.job_id, lines) logs = yaml.load(str(data), Loader=yaml.CLoader) if logs: print_logs(logs, options.raw, options.filters) lines += len(logs) # Loop only if the job is not finished if finished or options.no_follow: break # Wait some time time.sleep(options.polling) if finished: details = proxy.scheduler.jobs.show(options.job_id) if details.get("failure_comment"): print_logs([{"dt": datetime.datetime.utcnow().isoformat(), "lvl": "info", "msg": "[lavacli] Failure comment: %s" % details["failure_comment"]}], options.raw, options.filters) def handle_resubmit(proxy, options): job_id = proxy.scheduler.jobs.resubmit(options.job_id) if options.no_follow: if isinstance(job_id, list): for job in job_id: print(job) else: print(job_id) else: print_logs([{"dt": datetime.datetime.utcnow().isoformat(), "lvl": "info", "msg": "[lavacli] Job %s submitted" % job_id}], options.raw, options.filters) # Add the job_id to options for handle_logs # For multinode, print something and loop on all jobs if isinstance(job_id, list): for job in job_id: print_logs([{"dt": datetime.datetime.utcnow().isoformat(), "lvl": "info", "msg": "[lavacli] Seeing %s logs" % job}], options.raw, options.filters) options.job_id = job handle_logs(proxy, options) else: options.job_id = job_id handle_logs(proxy, options) def handle_run(proxy, options): job_id = proxy.scheduler.jobs.submit(options.definition.read()) print_logs([{"dt": datetime.datetime.utcnow().isoformat(), "lvl": "info", "msg": "[lavacli] Job %s submitted" % job_id}], options.raw, options.filters) # Add the job_id to options for handle_logs # For multinode, print something and loop on all jobs if isinstance(job_id, list): for job in job_id: print_logs([{"dt": datetime.datetime.utcnow().isoformat(), "lvl": "info", "msg": "[lavacli] Seeing %s logs" % job}], options.raw, options.filters) options.job_id = job handle_logs(proxy, options) else: options.job_id = job_id handle_logs(proxy, options) def handle_show(proxy, options): job = proxy.scheduler.jobs.show(options.job_id) if options.output_format == "yaml": job["submit_time"] = job["submit_time"].value job["start_time"] = job["start_time"].value job["end_time"] = job["end_time"].value print(yaml.dump(job).rstrip("\n")) else: print("id : %s" % job["id"]) print("description : %s" % job["description"]) print("submitter : %s" % job["submitter"]) print("device-type : %s" % job["device_type"]) print("device : %s" % job["device"]) print("health-check: %s" % job["health_check"]) print("state : %s" % job["state"]) print("Health : %s" % job["health"]) if job.get("failure_comment"): print("failure : %s" % job["failure_comment"]) print("pipeline : %s" % job["pipeline"]) print("tags : %s" % str(job["tags"])) print("visibility : %s" % job["visibility"]) print("submit time : %s" % job["submit_time"]) print("start time : %s" % job["start_time"]) print("end time : %s" % job["end_time"]) def handle_submit(proxy, options): job_id = proxy.scheduler.jobs.submit(options.definition.read()) if isinstance(job_id, list): for job in job_id: print(job) else: print(job_id) def handle_wait(proxy, options): job = proxy.scheduler.jobs.show(options.job_id) old_state = "" while job["state"] != "Finished": if old_state != job["state"]: if old_state: sys.stdout.write("\n") sys.stdout.write(job["state"]) else: sys.stdout.write(".") sys.stdout.flush() old_state = job["state"] time.sleep(options.polling) job = proxy.scheduler.jobs.show(options.job_id) def handle(proxy, options, _): handlers = { "cancel": handle_cancel, "definition": handle_definition, "list": handle_list, "logs": handle_logs, "resubmit": handle_resubmit, "run": handle_run, "show": handle_show, "submit": handle_submit, "wait": handle_wait, } return handlers[options.sub_sub_command](proxy, options) lavacli-0.7/lavacli/commands/system.py0000644000175000017500000002113413241511171021640 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see from contextlib import suppress import os import shutil import time import xmlrpc.client import yaml def configure_parser(parser): sub = parser.add_subparsers(dest="sub_sub_command", help="Sub commands") sub.required = True # "active" sub.add_parser("active", help="activate the system") # "api" sub.add_parser("api", help="print server API version") # "export" sys_export = sub.add_parser("export", help="export server configuration") sys_export.add_argument("name", help="name of the export") sys_export.add_argument("--full", default=False, action="store_true", help="do a full export, including retired devices") # "import" # sub.add_parser("import", help="import server configuration") # "maintenance" sys_maintenance = sub.add_parser("maintenance", help="maintenance the system") sys_maintenance.add_argument("--force", default=False, action="store_true", help="force maintenance by canceling jobs") sys_maintenance.add_argument("--exclude", default=None, help="comma seperated list of workers to keep untouched") # "methods" sys_methods = sub.add_parser("methods", help="list methods") sys_sub = sys_methods.add_subparsers(dest="sub_sub_sub_command", help="Sub commands") sys_sub.required = True sys_sub.add_parser("list", help="list available methods") sys_help = sys_sub.add_parser("help", help="method help") sys_help.add_argument("method", help="method name") sys_signature = sys_sub.add_parser("signature", help="method signature") sys_signature.add_argument("method", help="method name") # "version" sub.add_parser("version", help="print the server version") # "whoami" sub.add_parser("whoami", help="print the current username") def help_string(): return "system information" def handle_active(proxy, options): print("Activate workers:") workers = proxy.scheduler.workers.list() for worker in workers: print("* %s" % worker) proxy.scheduler.workers.update(worker, None, "ACTIVE") def handle_api(proxy, options): print(proxy.system.api_version()) def handle_export(proxy, options): print("Export to %s" % options.name) with suppress(FileNotFoundError): shutil.rmtree(options.name) os.mkdir(options.name) os.chdir(options.name) print("Listing aliases") aliases = [] for alias in proxy.scheduler.aliases.list(): print("* %s" % alias) aliases.append(proxy.scheduler.aliases.show(alias)) print("Listing tags") tags = [] for tag in proxy.scheduler.tags.list(): print("* %s" % tag["name"]) tags.append(tag) print("Listing workers") workers = [] for worker in proxy.scheduler.workers.list(): print("* %s" % worker) w = proxy.scheduler.workers.show(worker) workers.append({"hostname": w["hostname"], "description": w["description"], "state": w["state"], "health": w["health"]}) print("Listing device-types") os.mkdir("device-types") device_types = [] for device_type in proxy.scheduler.device_types.list(): print("* %s" % device_type["name"]) dt = proxy.scheduler.device_types.show(device_type["name"]) device_types.append({"name": dt["name"], "description": dt["description"], "display": dt["display"], "health_disabled": dt["health_disabled"], "owners_only": dt["owners_only"], "aliases": dt["aliases"]}) try: dt_template = proxy.scheduler.device_types.get_template(dt["name"]) except xmlrpc.client.Fault as exc: if exc.faultCode == 404: print(" => No template found") continue raise with open(os.path.join("device-types", dt["name"] + ".jinja2"), "w", encoding="utf-8") as f_out: f_out.write(str(dt_template)) print("Listing devices") os.mkdir("devices") devices = [] for device in proxy.scheduler.devices.list(options.full): print("* %s" % device["hostname"]) d = proxy.scheduler.devices.show(device["hostname"]) devices.append({"hostname": d["hostname"], "description": d["description"], "device_type": d["device_type"], "pipeline": d["pipeline"], "worker": d["worker"], "state": d["state"], "health": d["health"], "public": d["public"], "user": d["user"], "group": d["group"], "tags": d["tags"]}) try: device_dict = proxy.scheduler.devices.get_dictionary(device["hostname"]) except xmlrpc.client.Fault as exc: if exc.faultCode == 404: print(" => No device dict found") continue raise with open(os.path.join("devices", device["hostname"] + ".jinja2"), "w", encoding="utf-8") as f_out: f_out.write(str(device_dict)) export = {"aliases": aliases, "devices": devices, "device-types": device_types, "tags": tags, "workers": workers} # Dump the configuration with open("instance.yaml", "w", encoding="utf-8") as f_out: f_out.write(yaml.dump(export).rstrip("\n")) def handle_maintenance(proxy, options): print("Maintenance workers:") workers = proxy.scheduler.workers.list() excluded_workers = [] if options.exclude: excluded_workers = options.exclude.split(',') for worker in workers: if worker in excluded_workers: print("* %s [SKIP]" % worker) continue print("* %s" % worker) proxy.scheduler.workers.update(worker, None, "MAINTENANCE") excluded_devices = [] if excluded_workers: for worker in excluded_workers: excluded_devices.extend(proxy.scheduler.workers.show(worker)["devices"]) print("Wait for devices:") devices = proxy.scheduler.devices.list() for device in devices: if device["hostname"] in excluded_devices: continue print("* %s" % device["hostname"]) current_job = device["current_job"] if current_job is not None: print("--> waiting for job %s" % current_job) # if --force is passed, cancel the job if options.force: print("---> canceling") proxy.scheduler.jobs.cancel(current_job) while proxy.scheduler.jobs.show(current_job)["state"] != "Finished": print("---> waiting") time.sleep(5) def handle_methods(proxy, options): if options.sub_sub_sub_command == "help": print(proxy.system.methodHelp(options.method)) elif options.sub_sub_sub_command == "signature": print(proxy.system.methodSignature(options.method)) else: # Fallback to "list" methods = proxy.system.listMethods() for method in methods: print(method) def handle_version(proxy, _): print(proxy.system.version()) def handle_whoami(proxy, _): username = proxy.system.whoami() if username is None: print("") else: print(username) def handle(proxy, options, _): handlers = { "active": handle_active, "api": handle_api, "export": handle_export, "maintenance": handle_maintenance, "methods": handle_methods, "version": handle_version, "whoami": handle_whoami } return handlers[options.sub_sub_command](proxy, options) lavacli-0.7/lavacli/commands/devices.py0000644000175000017500000002760713241511171021751 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see import argparse import time import yaml def configure_parser(parser): sub = parser.add_subparsers(dest="sub_sub_command", help="Sub commands") sub.required = True # "add" devices_add = sub.add_parser("add", help="add a device") devices_add.add_argument("hostname", help="hostname of the device") devices_add.add_argument("--type", required=True, help="device-type") devices_add.add_argument("--worker", required=True, help="worker hostname") devices_add.add_argument("--description", default=None, help="device description") owner = devices_add.add_mutually_exclusive_group() owner.add_argument("--user", default=None, help="device owner") owner.add_argument("--group", default=None, help="device group owner") devices_add.add_argument("--health", default=None, choices=["GOOD", "UNKNOWN", "LOOPING", "BAD", "MAINTENANCE", "RETIRED"], help="device health") devices_add.add_argument("--private", action="store_true", default=False, help="private device, public by default") # "dict" devices_dict = sub.add_parser("dict", help="device dictionary") dict_sub = devices_dict.add_subparsers(dest="sub_sub_sub_command", help="Sub commands") dict_sub.required = True dict_get = dict_sub.add_parser("get", help="get the device dictionary") dict_get.add_argument("hostname", help="hostname of the device") dict_get.add_argument("field", nargs="?", default=None, help="only show the given sub-fields") dict_get.add_argument("--context", default=None, help="job context for template rendering") dict_get.add_argument("--render", action="store_true", default=False, help="render the dictionary into a configuration") dict_set = dict_sub.add_parser("set", help="set the device dictionary") dict_set.add_argument("hostname", help="hostname of the device") dict_set.add_argument("config", type=argparse.FileType('r'), help="device dictionary file") # "list" devices_list = sub.add_parser("list", help="list available devices") devices_list.add_argument("--all", "-a", action="store_true", default=False, help="list every devices, inluding retired") devices_list.add_argument("--yaml", dest="output_format", default=None, action="store_const", const="yaml", help="print as yaml") # "maintenance" devices_maintenance = sub.add_parser("maintenance", help="maintenance the device") devices_maintenance.add_argument("hostname", help="device hostname") devices_maintenance.add_argument("--force", default=False, action="store_true", help="force device maintenance by canceling running job") devices_maintenance.add_argument("--no-wait", dest="wait", default=True, action="store_false", help="do not wait for the device to be idle") # "show" devices_show = sub.add_parser("show", help="show device details") devices_show.add_argument("hostname", help="device hostname") devices_show.add_argument("--yaml", dest="output_format", action="store_const", const="yaml", default=None, help="print as yaml") # "tags" devices_tags = sub.add_parser("tags", help="manage tags for the given device") tags_sub = devices_tags.add_subparsers(dest="sub_sub_sub_command", help="Sub commands") tags_sub.required = True tags_add = tags_sub.add_parser("add", help="add a tag") tags_add.add_argument("hostname", help="hostname of the device") tags_add.add_argument("tag", help="name of the tag") tags_list = tags_sub.add_parser("list", help="list tags for the device") tags_list.add_argument("hostname", help="hostname of the device") tags_list.add_argument("--yaml", dest="output_format", action="store_const", const="yaml", help="print as yaml") tags_del = tags_sub.add_parser("delete", help="remove a tag") tags_del.add_argument("hostname", help="hostname of the device") tags_del.add_argument("tag", help="name of the tag") # "update" devices_update = sub.add_parser("update", help="update device properties") devices_update.add_argument("hostname", help="hostname of the device") devices_update.add_argument("--worker", default=None, help="worker hostname") devices_update.add_argument("--description", default=None, help="device description") owner = devices_update.add_mutually_exclusive_group() owner.add_argument("--user", default=None, help="device owner") owner.add_argument("--group", default=None, help="device group owner") devices_update.add_argument("--health", default=None, choices=["GOOD", "UNKNOWN", "LOOPING", "BAD", "MAINTENANCE", "RETIRED"], help="device health") display = devices_update.add_mutually_exclusive_group() display.add_argument("--public", default=None, action="store_true", help="make the device public") display.add_argument("--private", dest="public", action="store_false", help="make the device private") def help_string(): return "manage devices" def handle_add(proxy, options): proxy.scheduler.devices.add(options.hostname, options.type, options.worker, options.user, options.group, not options.private, options.health, options.description) def _lookups(value, fields): try: for key in fields: if isinstance(value, list): value = value[int(key)] else: value = value[key] except IndexError: print("list index out of range (%d vs %d)" % (int(key), len(value))) return 1 except KeyError as exc: print("Unknow key '%s' for '%s'" % (key, value)) return 1 print(value) return 0 def handle_dict(proxy, options): if options.sub_sub_sub_command == "get": config = proxy.scheduler.devices.get_dictionary(options.hostname, options.render, options.context) if not options.field: print(str(config).rstrip("\n")) return 0 if options.render: value = yaml.load(str(config)) return _lookups(value, options.field.split(".")) else: # Extract some variables import jinja2 env = jinja2.Environment() ast = env.parse(config) field_name = options.field.split(".")[0] # Loop on all assignments for assign in ast.find_all(jinja2.nodes.Assign): if assign.target.name == field_name: value = assign.node.as_const() if options.field == field_name: print(value) return 0 else: return _lookups(value, options.field.split(".")[1:]) print("Unknow field '%s'" % field_name) return 1 else: config = options.config.read() ret = proxy.scheduler.devices.set_dictionary(options.hostname, config) if not ret: print("Unable to set the configuration") return 0 if ret else 1 def handle_list(proxy, options): devices = proxy.scheduler.devices.list(options.all) if options.output_format == "yaml": print(yaml.dump(devices).rstrip("\n")) else: print("Devices:") for device in devices: print("* %s (%s): %s,%s" % (device["hostname"], device["type"], device["state"], device["health"])) def handle_maintenance(proxy, options): proxy.scheduler.devices.update(options.hostname, None, None, None, None, "MAINTENANCE", None) device = proxy.scheduler.devices.show(options.hostname) current_job = device["current_job"] if current_job is not None: print("-> waiting for job %s" % current_job) # if --force is passed, cancel the job if options.force: print("--> canceling") proxy.scheduler.jobs.cancel(current_job) while options.wait and proxy.scheduler.jobs.show(current_job)["state"] != "Finished": print("--> waiting") time.sleep(5) def handle_show(proxy, options): device = proxy.scheduler.devices.show(options.hostname) if options.output_format == "yaml": print(yaml.dump(device).rstrip("\n")) else: print("name : %s" % device["hostname"]) print("device-type : %s" % device["device_type"]) print("state : %s" % device["state"]) print("health : %s" % device["health"]) print("user : %s" % device["user"]) print("group : %s" % device["group"]) print("health : %s" % device["health"]) print("health job : %s" % device["health_job"]) print("description : %s" % device["description"]) print("public : %s" % device["public"]) print("pipeline : %s" % device["pipeline"]) print("device-dict : %s" % device["has_device_dict"]) print("worker : %s" % device["worker"]) print("current job : %s" % device["current_job"]) print("tags : %s" % device["tags"]) def handle_tags(proxy, options): if options.sub_sub_sub_command == "add": proxy.scheduler.devices.tags.add(options.hostname, options.tag) elif options.sub_sub_sub_command == "delete": proxy.scheduler.devices.tags.delete(options.hostname, options.tag) else: tags = proxy.scheduler.devices.tags.list(options.hostname) if options.output_format == "yaml": print(yaml.dump(tags).rstrip("\n")) else: print("Tags:") for tag in tags: print("* %s" % tag) def handle_update(proxy, options): proxy.scheduler.devices.update(options.hostname, options.worker, options.user, options.group, options.public, options.health, options.description) def handle(proxy, options, _): handlers = { "add": handle_add, "dict": handle_dict, "list": handle_list, "maintenance": handle_maintenance, "show": handle_show, "tags": handle_tags, "update": handle_update } return handlers[options.sub_sub_command](proxy, options) lavacli-0.7/lavacli/commands/aliases.py0000644000175000017500000000543413226116343021747 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see import yaml def configure_parser(parser): sub = parser.add_subparsers(dest="sub_sub_command", help="Sub commands") sub.required = True # "add" aliases_add = sub.add_parser("add", help="add an alias") aliases_add.add_argument("alias", help="alias name") # "delete" aliases_del = sub.add_parser("delete", help="delete an alias") aliases_del.add_argument("alias", help="alias name") # "list" aliases_list = sub.add_parser("list", help="list available aliases") aliases_list.add_argument("--yaml", dest="output_format", default=None, action="store_const", const="yaml", help="print as yaml") # "show" aliases_show = sub.add_parser("show", help="show alias details") aliases_show.add_argument("alias", help="alias") aliases_show.add_argument("--yaml", dest="output_format", action="store_const", const="yaml", default=None, help="print as yaml") def help_string(): return "manage device-type aliases" def handle_add(proxy, options): proxy.scheduler.aliases.add(options.alias) def handle_delete(proxy, options): proxy.scheduler.aliases.delete(options.alias) def handle_list(proxy, options): aliases = proxy.scheduler.aliases.list() if options.output_format == "yaml": print(yaml.dump(aliases).rstrip("\n")) else: print("Aliases:") for alias in aliases: print("* %s" % alias) def handle_show(proxy, options): alias = proxy.scheduler.aliases.show(options.alias) if options.output_format == "yaml": print(yaml.dump(alias).rstrip("\n")) else: print("name : %s" % alias["name"]) print("device-types:") for dt in alias["device_types"]: print("* %s" % dt) def handle(proxy, options, _): handlers = { "add": handle_add, "delete": handle_delete, "list": handle_list, "show": handle_show } return handlers[options.sub_sub_command](proxy, options) lavacli-0.7/lavacli/commands/device_types.py0000644000175000017500000002173313226116343023011 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see import argparse import yaml def configure_parser(parser): sub = parser.add_subparsers(dest="sub_sub_command", help="Sub commands") sub.required = True # "add" dt_add = sub.add_parser("add", help="add a device type") dt_add.add_argument("name", help="name of the device-type") dt_add.add_argument("--description", default=None, help="device-type description") dt_add.add_argument("--hide", dest="display", action="store_false", default=True, help="device is hidden in the UI") dt_add.add_argument("--owners-only", action="store_true", default=False, help="devices are only visible to owners") dt_health = dt_add.add_argument_group("health check") dt_health.add_argument("--health-frequency", default=24, help="how often to run health checks.") dt_health.add_argument("--health-denominator", default="hours", choices=["hours", "jobs"], help="initiate health checks by hours or by jobs.") # "aliases" dt_aliases = sub.add_parser("aliases", help="manage aliases for the given device-type") aliases_sub = dt_aliases.add_subparsers(dest="sub_sub_sub_command", help="Sub commands") aliases_sub.required = True aliases_add = aliases_sub.add_parser("add", help="add aliases") aliases_add.add_argument("name", help="name of the device-type") aliases_add.add_argument("alias", help="name of alias") aliases_delete = aliases_sub.add_parser("delete", help="delete aliases") aliases_delete.add_argument("name", help="name of the device-type") aliases_delete.add_argument("alias", help="name of alias") aliases_list = aliases_sub.add_parser("list", help="list aliases for the device-type") aliases_list.add_argument("name", help="device-type") aliases_list.add_argument("--yaml", dest="output_format", default=None, action="store_const", const="yaml", help="print as yaml") # "list" dt_list = sub.add_parser("list", help="list available device-types") dt_list.add_argument("--all", "-a", dest="show_all", default=False, action="store_true", help="show all device types in the database, " "including non-installed ones") dt_list.add_argument("--yaml", dest="output_format", default=None, action="store_const", const="yaml", help="print as yaml") # "show" dt_show = sub.add_parser("show", help="show device-type details") dt_show.add_argument("name", help="name of the device-type") dt_show.add_argument("--yaml", dest="output_format", action="store_const", const="yaml", default=None, help="print as yaml") # "template" devices_template = sub.add_parser("template", help="device-type template") dt_sub = devices_template.add_subparsers(dest="sub_sub_sub_command", help="Sub commands") dt_sub.required = True dt_get = dt_sub.add_parser("get", help="get the device-type template") dt_get.add_argument("name", help="name of the device-type") dt_set = dt_sub.add_parser("set", help="set the device-type template") dt_set.add_argument("name", help="name of the device-type") dt_set.add_argument("template", type=argparse.FileType('r'), help="template file") # "update" dt_update = sub.add_parser("update", help="update device-type") dt_update.add_argument("name", help="name of the device-type") dt_update.add_argument("--description", default=None, help="device-type description") visibility = dt_update.add_mutually_exclusive_group() visibility.add_argument("--hide", dest="display", action="store_false", default=None, help="device-type is hidden in the UI") visibility.add_argument("--show", dest="display", action="store_true", help="device-type is visible in the UI") owner = dt_update.add_mutually_exclusive_group() owner.add_argument("--owners-only", action="store_true", dest="owners_only", default=None, help="devices are only visible to owners") owner.add_argument("--public", action="store_false", dest="owners_only", help="devices are visible to all users") dt_health = dt_update.add_argument_group("health check") dt_health.add_argument("--health-frequency", default=None, help="how often to run health checks.") dt_health.add_argument("--health-denominator", default=None, choices=["hours", "jobs"], help="initiate health checks by hours or by jobs.") health = dt_health.add_mutually_exclusive_group() health.add_argument("--health-disabled", default=None, action="store_true", help="disable health checks") health.add_argument("--health-active", dest="health_disabled", action="store_false", help="activate health checks") def help_string(): return "manage device-types" def handle_add(proxy, options): proxy.scheduler.device_types.add(options.name, options.description, options.display, options.owners_only, options.health_frequency, options.health_denominator) def handle_aliases(proxy, options): if options.sub_sub_sub_command == "add": proxy.scheduler.device_types.aliases.add(options.name, options.alias) elif options.sub_sub_sub_command == "list": aliases = proxy.scheduler.device_types.aliases.list(options.name) if options.output_format == "yaml": print(yaml.dump(aliases).rstrip("\n")) else: print("Aliases:") for alias in aliases: print("* %s" % alias) elif options.sub_sub_sub_command == "delete": proxy.scheduler.device_types.aliases.delete(options.name, options.alias) def handle_list(proxy, options): device_types = proxy.scheduler.device_types.list(options.show_all) if options.output_format == "yaml": print(yaml.dump(device_types).rstrip("\n")) else: print("Device-Types:") for dt in device_types: print("* %s (%s)" % (dt["name"], dt["devices"])) def handle_show(proxy, options): dt = proxy.scheduler.device_types.show(options.name) if options.output_format == "yaml": print(yaml.dump(dt).rstrip("\n")) else: print("name : %s" % dt["name"]) print("description : %s" % dt["description"]) print("display : %s" % dt["display"]) print("owners only : %s" % dt["owners_only"]) print("health disabled : %s" % dt["health_disabled"]) print("aliases : %s" % dt["aliases"]) print("devices : %s" % dt["devices"]) def handle_template(proxy, options): if options.sub_sub_sub_command == "get": template = proxy.scheduler.device_types.get_template(options.name) print(str(template).rstrip("\n")) else: template = options.template.read() proxy.scheduler.device_types.set_template(options.name, template) def handle_update(proxy, options): proxy.scheduler.device_types.update(options.name, options.description, options.display, options.owners_only, options.health_frequency, options.health_denominator, options.health_disabled) def handle(proxy, options, _): handlers = { "add": handle_add, "aliases": handle_aliases, "list": handle_list, "show": handle_show, "template": handle_template, "update": handle_update } return handlers[options.sub_sub_command](proxy, options) lavacli-0.7/lavacli/commands/tags.py0000644000175000017500000000572113226116343021263 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see import yaml def configure_parser(parser): sub = parser.add_subparsers(dest="sub_sub_command", help="Sub commands") sub.required = True # "add" tags_add = sub.add_parser("add", help="add a tag") tags_add.add_argument("tag", help="tag name") tags_add.add_argument("--description", default=None, help="tag description") # "delete" tags_delete = sub.add_parser("delete", help="delete a tag") tags_delete.add_argument("tag", help="tag name") # "list" tags_list = sub.add_parser("list", help="list tags") tags_list.add_argument("--yaml", dest="output_format", action="store_const", const="yaml", help="print as yaml") # "show" tags_show = sub.add_parser("show", help="show tag details") tags_show.add_argument("tag", help="tag name") tags_show.add_argument("--yaml", dest="output_format", action="store_const", const="yaml", help="print as yaml") def help_string(): return "manage device tags" def handle_add(proxy, options): proxy.scheduler.tags.add(options.tag, options.description) def handle_delete(proxy, options): proxy.scheduler.tags.delete(options.tag) def handle_list(proxy, options): tags = proxy.scheduler.tags.list() if options.output_format == "yaml": print(yaml.dump(tags).rstrip("\n")) else: print("Tags:") for tag in tags: if tag["description"]: print("* %s (%s)" % (tag["name"], tag["description"])) else: print("* %s" % tag["name"]) def handle_show(proxy, options): tag = proxy.scheduler.tags.show(options.tag) if options.output_format == "yaml": print(yaml.dump(tag).rstrip("\n")) else: print("name : %s" % tag["name"]) print("description: %s" % tag["description"]) print("devices :") for device in tag["devices"]: print("* %s" % device) def handle(proxy, options, _): handlers = { "add": handle_add, "delete": handle_delete, "list": handle_list, "show": handle_show } return handlers[options.sub_sub_command](proxy, options) lavacli-0.7/lavacli/commands/events.py0000644000175000017500000001654713226116343021641 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see import json from urllib.parse import urlparse import zmq from zmq.utils.strtypes import b, u def configure_parser(parser): sub = parser.add_subparsers(dest="sub_sub_command", help="Sub commands") sub.required = True # "listen" sub.add_parser("listen", help="listen to events") # "wait" wait_parser = sub.add_parser("wait", help="wait for a specific event") obj_parser = wait_parser.add_subparsers(dest="object", help="object to wait") obj_parser.required = True # "wait device" device_parser = obj_parser.add_parser("device") device_parser.add_argument("hostname", type=str, help="hostname of the device") device_parser.add_argument("--state", default=None, choices=["IDLE", "RESERVED", "RUNNING"], help="device state") device_parser.add_argument("--health", default=None, choices=["GOOD", "UNKNOWN", "LOOPING", "BAD", "MAINTENANCE", "RETIRED"], help="device health") # "wait job" testjob_parser = obj_parser.add_parser("job") testjob_parser.add_argument("job_id", help="job id") testjob_parser.add_argument("--state", default=None, choices=["SUBMITTED", "SCHEDULING", "SCHEDULED", "RUNNING", "CANCELING", "FINISHED"], help="job state") testjob_parser.add_argument("--health", default=None, choices=["UNKNOWN", "COMPLETE", "INCOMPLETE", "CANCELED"], help="job health") # "wait worker" worker_parser = obj_parser.add_parser("worker") worker_parser.add_argument("hostname", type=str, help="worker hostname") worker_parser.add_argument("--state", default=None, choices=["ONLINE", "OFFLINE"], help="worker state") worker_parser.add_argument("--health", default=None, choices=["ACTIVE", "MAINTENANCE", "RETIRED"], help="worker health") def help_string(): return "listen to events" def _get_zmq_url(proxy, options, config): if config is None or config.get("events", {}).get("uri") is None: url = proxy.scheduler.get_publisher_event_socket() if '*' in url: domain = urlparse(options.uri).netloc if '@' in domain: domain = domain.split('@')[1] domain = domain.split(':')[0] url = url.replace('*', domain) else: url = config["events"]["uri"] return url def handle_listen(proxy, options, config): # Try to find the socket url url = _get_zmq_url(proxy, options, config) if url is None: print("Unable to find the socket url") return 1 context = zmq.Context() sock = context.socket(zmq.SUB) sock.setsockopt(zmq.SUBSCRIBE, b"") # Set the sock proxy (if needed) socks = config.get("events", {}).get("socks_proxy") if socks is not None: print("Listening to %s (socks %s)" % (url, socks)) sock.setsockopt(zmq.SOCKS_PROXY, b(socks)) else: print("Listening to %s" % url) try: sock.connect(url) except zmq.error.ZMQError as exc: print("Unable to connect: %s" % exc) return 1 while True: msg = sock.recv_multipart() try: (topic, _, dt, username, data) = (u(m) for m in msg) except ValueError: print("Invalid message: %s" % msg) continue # If unknown, print the full data msg = data data = json.loads(data) # Print according to the topic topic_end = topic.split(".")[-1] if topic_end == "device": msg = "[%s] <%s> state=%s health=%s" % (data["device"], data["device_type"], data["state"], data["health"]) if "job" in data: msg += " for %s" % data["job"] elif topic_end == "testjob": msg = "[%s] <%s> state=%s health=%s (%s)" % (data["job"], data.get("device", "??"), data["state"], data["health"], data["description"]) elif topic_end == "worker": msg = "[%s] state=%s health=%s" % (data["hostname"], data["state"], data["health"]) print("\033[1;30m%s\033[0m \033[1;37m%s\033[0m \033[32m%s\033[0m - %s" % (dt, topic, username, msg)) def handle_wait(proxy, options, config): # Try to find the socket url url = _get_zmq_url(proxy, options, config) if url is None: print("Unable to find the socket url") return 1 context = zmq.Context() sock = context.socket(zmq.SUB) # Filter by topic (if needed) sock.setsockopt(zmq.SUBSCRIBE, b(config.get("events", {}).get("topic", ""))) # Set the sock proxy (if needed) socks = config.get("events", {}).get("socks_proxy") if socks is not None: print("Listening to %s (socks %s)" % (url, socks)) sock.setsockopt(zmq.SOCKS_PROXY, b(socks)) else: print("Listening to %s" % url) try: sock.connect(url) except zmq.error.ZMQError as exc: print("Unable to connect: %s" % exc) return 1 # "job" is called "testjob" in the events object_topic = options.object if object_topic == "job": object_topic = "testjob" # Wait for events while True: msg = sock.recv_multipart() try: (topic, _, dt, username, data) = (u(m) for m in msg) except ValueError: print("Invalid message: %s" % msg) continue data = json.loads(data) # Filter by object obj = topic.split(".")[-1] if obj != object_topic: continue if object_topic == "device": if data.get("device") != options.hostname: continue elif object_topic == "testjob": if data.get("job") != options.job_id: continue else: if data.get("hostname") != options.hostname: continue # Filter by state if options.state is not None: if data.get("state") != options.state.capitalize(): continue # Filter by health if options.health is not None: if data.get("health") != options.health.capitalize(): continue return 0 def handle(proxy, options, config): handlers = { "listen": handle_listen, "wait": handle_wait } return handlers[options.sub_sub_command](proxy, options, config) lavacli-0.7/lavacli/commands/__init__.py0000644000175000017500000000000013226116343022045 0ustar stylesenstylesen00000000000000lavacli-0.7/lavacli/__init__.py0000644000175000017500000001603013241511171020251 0ustar stylesenstylesen00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see import argparse import os import requests import socket import sys from urllib.parse import urlparse import xmlrpc.client import yaml from .__about__ import * from .commands import ( aliases, devices, device_types, events, identities, jobs, results, system, tags, utils, workers ) class RequestsTransport(xmlrpc.client.Transport): def __init__(self, scheme, proxy=None, timeout=20.0, verify_ssl_cert=True): super().__init__() self.scheme = scheme # Set the user agent self.user_agent = "lavacli v%s" % __version__ if proxy is None: self.proxies = {} else: self.proxies = {scheme: proxy} self.timeout = timeout self.verify_ssl_cert = verify_ssl_cert if not verify_ssl_cert: from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) def request(self, host, handler, data, verbose=False): headers = {"User-Agent": self.user_agent, "Content-Type": "text/xml", "Accept-Encoding": "gzip"} url = "%s://%s%s" % (self.scheme, host, handler) try: response = None response = requests.post(url, data=data, headers=headers, timeout=self.timeout, verify=self.verify_ssl_cert, proxies=self.proxies) response.raise_for_status() return self.parse_response(response) except requests.RequestException as e: if response is None: raise xmlrpc.client.ProtocolError(url, 500, str(e), "") else: raise xmlrpc.client.ProtocolError(url, response.status_code, str(e), response.headers) def parse_response(self, resp): """ Parse the xmlrpc response. """ p, u = self.getparser() p.feed(resp.text) p.close() return u.close() def load_config(identity): # Build the path to the configuration file config_dir = os.environ.get("XDG_CONFIG_HOME", "~/.config") config_filename = os.path.expanduser(os.path.join(config_dir, "lavacli.yaml")) try: with open(config_filename, "r", encoding="utf-8") as f_conf: config = yaml.load(f_conf.read()) return config[identity] except (FileNotFoundError, KeyError, TypeError): return None def parser(commands): parser_obj = argparse.ArgumentParser() # "--version" parser_obj.add_argument("--version", action="store_true", default=False, help="print the version number and exit") # identity or url url = parser_obj.add_mutually_exclusive_group() url.add_argument("--uri", type=str, default=None, help="URI of the lava-server RPC endpoint") url.add_argument("--identity", "-i", metavar="ID", type=str, default="default", help="identity stored in the configuration") # The sub commands root = parser_obj.add_subparsers(dest="sub_command", help="Sub commands") keys = list(commands.keys()) keys.sort() for name in keys: cls = commands[name] cls.configure_parser(root.add_parser(name, help=cls.help_string())) return parser_obj def main(): commands = {"aliases": aliases, "devices": devices, "device-types": device_types, "events": events, "identities": identities, "jobs": jobs, "results": results, "system": system, "tags": tags, "utils": utils, "workers": workers} # Parse the command line parser_obj = parser(commands) options = parser_obj.parse_args() # Do we have to print the version numer? if options.version: print("lavacli %s" % __version__) return # Print help if lavacli is called without any arguments if len(sys.argv) == 1: parser_obj.print_help() return 1 if options.uri is None: config = load_config(options.identity) if config is None: print("Unknown identity '%s'" % options.identity) return 1 username = config.get("username") token = config.get("token") if username is not None and token is not None: p = urlparse(config["uri"]) options.uri = "%s://%s:%s@%s%s" % (p.scheme, username, token, p.netloc, p.path) else: options.uri = config["uri"] else: config = {} # Check that a sub_command was given if options.sub_command is None: parser_obj.print_help() return 1 # Create the Transport object parsed_uri = urlparse(options.uri) if options.identity: transport = RequestsTransport(parsed_uri.scheme, config.get("proxy"), config.get("timeout", 20.0), config.get("verify_ssl_cert", True)) else: transport = RequestsTransport(parsed_uri.scheme) # Connect to the RPC endpoint try: # allow_none is True because the server does support it proxy = xmlrpc.client.ServerProxy(options.uri, allow_none=True, transport=transport) return commands[options.sub_command].handle(proxy, options, config) except (ConnectionError, socket.gaierror) as exc: print("Unable to connect to '%s': %s" % (options.uri, str(exc))) except KeyboardInterrupt: pass except xmlrpc.client.Error as exc: if "sub_sub_command" in options: print("Unable to call '%s.%s': %s" % (options.sub_command, options.sub_sub_command, str(exc))) else: print("Unable to call '%s': %s" % (options.sub_command, str(exc))) except BaseException as exc: print("Unknown error when connecting to '%s': %s" % (options.uri, str(exc))) return 1 lavacli-0.7/lavacli.egg-info/0000755000175000017500000000000013241511244017633 5ustar stylesenstylesen00000000000000lavacli-0.7/lavacli.egg-info/requires.txt0000644000175000017500000000002613241511244022231 0ustar stylesenstylesen00000000000000PyYAML pyzmq requests lavacli-0.7/lavacli.egg-info/dependency_links.txt0000644000175000017500000000000113241511244023701 0ustar stylesenstylesen00000000000000 lavacli-0.7/lavacli.egg-info/zip-safe0000644000175000017500000000000113236773162021300 0ustar stylesenstylesen00000000000000 lavacli-0.7/lavacli.egg-info/entry_points.txt0000644000175000017500000000005213241511244023126 0ustar stylesenstylesen00000000000000[console_scripts] lavacli = lavacli:main lavacli-0.7/lavacli.egg-info/SOURCES.txt0000644000175000017500000000117313241511244021521 0ustar stylesenstylesen00000000000000setup.py lavacli/__about__.py lavacli/__init__.py lavacli/__main__.py lavacli/utils.py lavacli.egg-info/PKG-INFO lavacli.egg-info/SOURCES.txt lavacli.egg-info/dependency_links.txt lavacli.egg-info/entry_points.txt lavacli.egg-info/requires.txt lavacli.egg-info/top_level.txt lavacli.egg-info/zip-safe lavacli/commands/__init__.py lavacli/commands/aliases.py lavacli/commands/device_types.py lavacli/commands/devices.py lavacli/commands/events.py lavacli/commands/identities.py lavacli/commands/jobs.py lavacli/commands/results.py lavacli/commands/system.py lavacli/commands/tags.py lavacli/commands/utils.py lavacli/commands/workers.pylavacli-0.7/lavacli.egg-info/PKG-INFO0000644000175000017500000000155113241511244020732 0ustar stylesenstylesen00000000000000Metadata-Version: 1.1 Name: lavacli Version: 0.7 Summary: LAVA XML-RPC command line interface Home-page: https://git.linaro.org/lava/lavacli.git Author: Rémi Duraffort Author-email: ivoire@videolan.org License: AGPLv3+ Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Topic :: Communications Classifier: Topic :: Software Development :: Testing Classifier: Topic :: System :: Networking lavacli-0.7/lavacli.egg-info/top_level.txt0000644000175000017500000000001013241511244022354 0ustar stylesenstylesen00000000000000lavacli lavacli-0.7/PKG-INFO0000644000175000017500000000155113241511244015625 0ustar stylesenstylesen00000000000000Metadata-Version: 1.1 Name: lavacli Version: 0.7 Summary: LAVA XML-RPC command line interface Home-page: https://git.linaro.org/lava/lavacli.git Author: Rémi Duraffort Author-email: ivoire@videolan.org License: AGPLv3+ Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Topic :: Communications Classifier: Topic :: Software Development :: Testing Classifier: Topic :: System :: Networking lavacli-0.7/setup.cfg0000644000175000017500000000004613241511244016347 0ustar stylesenstylesen00000000000000[egg_info] tag_build = tag_date = 0 lavacli-0.7/setup.py0000644000175000017500000000405413226116343016247 0ustar stylesenstylesen00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # vim: set ts=4 # Copyright 2017 Rémi Duraffort # This file is part of lavacli. # # lavacli is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # lavacli 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with lavacli. If not, see from setuptools import setup # grab metadata without importing the module metadata = {} with open("lavacli/__about__.py", encoding="utf-8") as fp: exec(fp.read(), metadata) # Setup the package setup( name='lavacli', version=metadata['__version__'], description=metadata['__description__'], author=metadata['__author__'], author_email='ivoire@videolan.org', license=metadata['__license__'], url=metadata['__url__'], classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3 :: Only", "Topic :: Communications", "Topic :: Software Development :: Testing", "Topic :: System :: Networking", ], packages=['lavacli', 'lavacli.commands'], entry_points={ 'console_scripts': [ 'lavacli = lavacli:main' ] }, install_requires=[ "PyYAML", "pyzmq", "requests", ], zip_safe=True )