Index: lnt/trunk/docs/api.rst =================================================================== --- lnt/trunk/docs/api.rst +++ lnt/trunk/docs/api.rst @@ -37,6 +37,8 @@ | /samples/`id` | Get all non-empty sample info for Sample `id`. | +---------------------------+------------------------------------------------------------------------------------------+ +.. _auth_tokens: + Write Operations ---------------- Index: lnt/trunk/docs/tools.rst =================================================================== --- lnt/trunk/docs/tools.rst +++ lnt/trunk/docs/tools.rst @@ -44,6 +44,49 @@ Run a built-in test. See the :ref:`tests` documentation for more details on this tool. + +Server Administration +~~~~~~~~~~~~~~~~~~~~~ + +The ``lnt admin`` tool allows connecting to a server through LNTs REST API and +perform data queries and modifications. Data modification is only possible with +an authentication mechanism specified in the `lntadmin.cfg` file. See +:ref:`auth_tokens` for details. + + ``lnt admin create-config`` + Create a `lntadmin.cfg` configuration file in the current directory. The file + describes the URL, authentication settings and default database and + test-suite settings for an LNT server. The other admin commands read this + file if it exists. + + ``lnt admin list-machines`` + List machines and their id numbers + + ``lnt admin get-machine `` + Download machine information and run list for a specific machine. + + ``lnt admin rm-machine `` + Removes the specified machine and related runs and samples. + + ``lnt admin rename-machine `` + Renames the specified machine. + + ``lnt admin merge-machine-into `` + Move all runs from one machine to another machine and delete the machine. + + ``lnt admin list-runs `` + List all runs for the specified machine. + + ``lnt admin get-run +`` + Download the specified runs. + + ``lnt admin post-run +`` + Post the specified report files as a new runs to the server. + + ``lnt admin rm-run +`` + Remove the specified runs and related samples. + + Server-Side Tools ----------------- Index: lnt/trunk/lnt/lnttool/admin.py =================================================================== --- lnt/trunk/lnt/lnttool/admin.py +++ lnt/trunk/lnt/lnttool/admin.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python +import click + +_config_filename = 'lntadmin.yaml' + + +def _load_dependencies(): + global yaml, sys, requests, json, os, httplib + import yaml + import sys + import requests + import json + import os + import httplib + + +def _error(msg): + sys.stderr.write('%s\n' % msg) + + +def _fatal(msg): + _error(msg) + sys.exit(1) + + +def _check_normalize_config(config, need_auth_token): + '''Verify whether config is correct and complete. Also normalizes the + server URL if necessary.''' + lnt_url = config.get('lnt_url', None) + if lnt_url is None: + _fatal('No lnt_url specified in config or commandline\n' + 'Tip: Use `create-config` for an example configuration') + if lnt_url.endswith('/'): + lnt_url = lnt_url[:-1] + config['lnt_url'] = lnt_url + database = config.get('database', None) + if database is None: + _fatal('No database specified in config or commandline') + testsuite = config.get('testsuite', None) + if testsuite is None: + config['testsuite'] = 'nts' + + session = requests.Session() + user = config.get('user', None) + password = config.get('password', None) + if user is not None and password is not None: + session.auth = (user, password) + + auth_token = config.get('auth_token', None) + if need_auth_token and auth_token is None: + _fatal('No auth_token specified in config') + else: + session.headers.update({'AuthToken': auth_token}) + config['session'] = session + + +def _make_config(kwargs, need_auth_token=False): + '''Load configuration from yaml file, merges it with the commandline + options and verifies the resulting configuration.''' + verbose = kwargs.get('verbose', False) + # Load config file + config = {} + try: + config = yaml.load(open(_config_filename)) + except IOError: + if verbose: + _error("Could not load configuration file '%s'\n" % + _config_filename) + for key, value in kwargs.items(): + if value is None: + continue + config[key] = value + _check_normalize_config(config, need_auth_token=need_auth_token) + return config + + +def _check_response(response): + '''Check given response. If it is not a 200 response print an error message + and quit.''' + status_code = response.status_code + if 200 <= status_code and status_code < 400: + return + + sys.stderr.write("%d: %s\n" % + (status_code, httplib.responses.get(status_code, ''))) + sys.stderr.write("\n%s\n" % response.text) + sys.exit(1) + + +def _print_machine_info(machine, indent=''): + for key, value in machine.items(): + sys.stdout.write('%s%s: %s\n' % (indent, key, value)) + + +def _print_run_info(run, indent=''): + for key, value in run.items(): + sys.stdout.write('%s%s: %s\n' % (indent, key, value)) + + +def _common_options(func): + func = click.option("--lnt-url", help="URL of LNT server")(func) + func = click.option("--database", help="database to use")(func) + func = click.option("--testsuite", help="testsuite to use")(func) + func = click.option("--verbose", "-v", is_flag=True, + help="verbose output")(func) + return func + + +@click.command("list-machines") +@_common_options +def action_list_machines(**kwargs): + """List machines and their id numbers.""" + config = _make_config(kwargs) + + url = ('{lnt_url}/api/db_{database}/v4/{testsuite}/machines' + .format(**config)) + session = config['session'] + response = session.get(url) + _check_response(response) + data = json.loads(response.text) + for machine in data['machines']: + id = machine.get('id', None) + name = machine.get('name', None) + sys.stdout.write("%s:%s\n" % (name, id)) + if config['verbose']: + _print_machine_info(machine, indent='\t') + + +@click.command("get-machine") +@click.argument("machine") +@_common_options +def action_get_machine(**kwargs): + """Download machine information and run list.""" + config = _make_config(kwargs) + + filename = 'machine_%s.json' % config['machine'] + if os.path.exists(filename): + _fatal("'%s' already exists" % filename) + + url = ('{lnt_url}/api/db_{database}/v4/{testsuite}/machines/{machine}' + .format(**config)) + session = config['session'] + response = session.get(url) + _check_response(response) + data = json.loads(response.text) + assert len(data['machines']) == 1 + machine = data['machines'][0] + + result = { + 'machine': machine + } + runs = data.get('runs', None) + if runs is not None: + result['runs'] = runs + with open(filename, "w") as destfile: + json.dump(result, destfile, indent=2) + sys.stdout.write("%s created.\n" % filename) + + +@click.command("rm-machine") +@click.argument("machine") +@_common_options +def action_rm_machine(**kwargs): + """Remove machine and related data.""" + config = _make_config(kwargs, need_auth_token=True) + + url = ('{lnt_url}/api/db_{database}/v4/{testsuite}/machines/{machine}' + .format(**config)) + session = config['session'] + response = session.delete(url, stream=True) + _check_response(response) + for line in response.iter_lines(): + sys.stdout.write(line + '\n') + sys.stdout.flush() + + +@click.command("rename-machine") +@click.argument("machine") +@click.argument("new-name") +@_common_options +def action_rename_machine(**kwargs): + """Rename machine.""" + config = _make_config(kwargs, need_auth_token=True) + + url = ('{lnt_url}/api/db_{database}/v4/{testsuite}/machines/{machine}' + .format(**config)) + session = config['session'] + response = session.post(url, data=(('action', 'rename'), + ('name', config['new_name']))) + _check_response(response) + + +@click.command("merge-machine-into") +@click.argument("machine") +@click.argument("into") +@_common_options +def action_merge_machine_into(**kwargs): + """Merge machine into another machine.""" + config = _make_config(kwargs, need_auth_token=True) + + url = ('{lnt_url}/api/db_{database}/v4/{testsuite}/machines/{machine}' + .format(**config)) + session = config['session'] + response = session.post(url, data=(('action', 'merge'), + ('into', config['into']))) + _check_response(response) + + +@click.command("list-runs") +@click.argument("machine") +@_common_options +def action_list_runs(**kwargs): + """List runs of a machine.""" + config = _make_config(kwargs) + + url = ('{lnt_url}/api/db_{database}/v4/{testsuite}/machines/{machine}' + .format(**config)) + session = config['session'] + response = session.get(url) + _check_response(response) + data = json.loads(response.text) + runs = data['runs'] + if config['verbose']: + sys.stdout.write("order run-id\n") + sys.stdout.write("------------\n") + for run in runs: + order_by = [x.strip() for x in run['order_by'].split(',')] + orders = [] + for field in order_by: + orders.append("%s=%s" % (field, run[field])) + sys.stdout.write("%s %s\n" % (";".join(orders), run['id'])) + if config['verbose']: + _print_run_info(run, indent='\t') + + +@click.command("get-run") +@click.argument("runs", nargs=-1, required=True) +@_common_options +def action_get_run(**kwargs): + """Download runs and save as report files.""" + config = _make_config(kwargs) + + runs = config['runs'] + for run in runs: + filename = 'run_%s.json' % run + if os.path.exists(filename): + _fatal("'%s' already exists" % filename) + + session = config['session'] + for run in runs: + url = ('{lnt_url}/api/db_{database}/v4/{testsuite}/runs/{run}' + .format(run=run, **config)) + response = session.get(url) + _check_response(response) + + data = json.loads(response.text) + filename = 'run_%s.json' % run + with open(filename, "w") as destfile: + json.dump(data, destfile, indent=2) + sys.stdout.write("%s created.\n" % filename) + + +@click.command("rm-run") +@click.argument("runs", nargs=-1, required=True) +@_common_options +def action_rm_run(**kwargs): + """Remove runs and related data.""" + config = _make_config(kwargs, need_auth_token=True) + + session = config['session'] + runs = config['runs'] + for run in runs: + url = ('{lnt_url}/api/db_{database}/v4/{testsuite}/runs/{run}' + .format(run=run, **config)) + response = session.delete(url) + _check_response(response) + + +@click.command("post-run") +@click.argument("datafiles", nargs=-1, type=click.Path(exists=True), + required=True) +@_common_options +@click.option("--update-machine", is_flag=True, help="Update machine fields") +@click.option("--merge", default="replace", show_default=True, + type=click.Choice(['reject', 'replace', 'merge']), + help="Merge strategy when run already exists") +def action_post_run(**kwargs): + """Submit report files to server.""" + config = _make_config(kwargs, need_auth_token=True) + + session = config['session'] + datafiles = config['datafiles'] + for datafile in datafiles: + with open(datafile, "r") as datafile: + data = datafile.read() + + url = ('{lnt_url}/api/db_{database}/v4/{testsuite}/runs' + .format(**config)) + url_params = { + 'update_machine': 1 if config['update_machine'] else 0, + 'merge': config['merge'], + } + response = session.post(url, params=url_params, data=data, + allow_redirects=False) + _check_response(response) + if response.status_code == 301: + sys.stdout.write(response.headers.get('Location') + '\n') + if config['verbose']: + try: + response_data = json.loads(response.text) + json.dump(response_data, sys.stderr, response_data, indent=2) + except: + sys.stderr.write(response.text) + sys.stderr.write('\n') + + +@click.command('create-config') +def action_create_config(): + """Create example configuration.""" + if os.path.exists(_config_filename): + _fatal("'%s' already exists" % _config_filename) + with open(_config_filename, "w") as out: + out.write('''\ +lnt_url: "http://localhost:8000" +database: default +testsuite: nts +# user: 'http_user' +# password: 'http_password' +# auth_token: 'secret' +''') + sys.stderr.write("Created '%s'\n" % _config_filename) + + +class AdminCLI(click.MultiCommand): + '''Admin subcommands. Put into this class so we can lazily import + dependencies.''' + _commands = [ + action_create_config, + action_get_machine, + action_get_run, + action_list_machines, + action_list_runs, + action_merge_machine_into, + action_post_run, + action_rename_machine, + action_rm_machine, + action_rm_run, + ] + def list_commands(self, ctx): + return [command.name for command in self._commands] + + def get_command(self, ctx, name): + _load_dependencies() + for command in self._commands: + if command.name == name: + return command + raise ValueError("Request unknown command '%s'" % name) + + +@click.group("admin", cls=AdminCLI, no_args_is_help=True) +def group_admin(): + """LNT server admin client.""" Index: lnt/trunk/lnt/lnttool/main.py =================================================================== --- lnt/trunk/lnt/lnttool/main.py +++ lnt/trunk/lnt/lnttool/main.py @@ -6,6 +6,7 @@ from .import_report import action_importreport from .updatedb import action_updatedb from .viewcomparison import action_view_comparison +from .admin import group_admin from lnt.util import logger import click import logging @@ -120,7 +121,7 @@ @click.group("runtest", cls=RunTestCLI, context_settings=dict( ignore_unknown_options=True, allow_extra_args=True,)) -def action_runtest(): +def group_runtest(): """run a builtin test application""" init_logger(logging.INFO) @@ -462,13 +463,12 @@ Use ``lnt --help`` for more information on a specific command. """ cli.add_command(action_checkformat) -cli.add_command(action_create) cli.add_command(action_convert) +cli.add_command(action_create) cli.add_command(action_import) cli.add_command(action_importreport) cli.add_command(action_profile) cli.add_command(action_runserver) -cli.add_command(action_runtest) cli.add_command(action_send_daily_report) cli.add_command(action_send_run_comparison) cli.add_command(action_showtests) @@ -476,6 +476,8 @@ cli.add_command(action_update) cli.add_command(action_updatedb) cli.add_command(action_view_comparison) +cli.add_command(group_admin) +cli.add_command(group_runtest) def main(): Index: lnt/trunk/tests/lnttool/admin.shtest =================================================================== --- lnt/trunk/tests/lnttool/admin.shtest +++ lnt/trunk/tests/lnttool/admin.shtest @@ -0,0 +1,70 @@ +# RUN: rm -f %T/lntadmin.yaml +# RUN: cd %T ; lnt admin create-config +# RUN: FileCheck %s < %T/lntadmin.yaml --check-prefix=CREATE_CONFIG +# CREATE_CONFIG: lnt_url: "http://localhost:8000" +# CREATE_CONFIG-NEXT: database: default +# CREATE_CONFIG-NEXT: testsuite: nts +# CREATE_CONFIG-NEXT: # user: 'http_user' +# CREATE_CONFIG-NEXT: # password: 'http_password' +# CREATE_CONFIG-NEXT: # auth_token: 'secret' + +# RUN: rm -rf %t.instance +# RUN: python %{shared_inputs}/create_temp_instance.py \ +# RUN: %s %{shared_inputs}/SmallInstance %t.instance +# RUN: %{shared_inputs}/server_wrapper.sh %t.instance 9092 /bin/sh %s %T %{shared_inputs} + +DIR="$1" +SHARED_INPUTS="$2" +cd "$DIR" +cat > lntadmin.yaml << '__EOF__' +lnt_url: "http://localhost:9092" +database: default +testsuite: nts +auth_token: test_token +__EOF__ + +lnt admin post-run "${SHARED_INPUTS}/sample-a-small.plist" > post_run.stdout +# RUN: FileCheck %s --check-prefix=POST_RN < %T/post_run.stdout +# POST_RN: http://localhost:9092/api/db_default/v4/nts/runs/3 + +rm -rf run_3.json +lnt admin get-run 3 +# RUN: FileCheck %s --check-prefix=GET_RN < %T/run_3.json +# GET_RN: { +# GET_RN: "machine": { +# ... +# GET_RN: }, +# GET_RN: "tests": [ +#... +# GET_RN: ], +# GET_RN: "run": { +# GET_RN: "start_time": "2009-11-17T02:12:25" +# GET_RN: "end_time": "2009-11-17T03:44:48" +# GET_RN: "id": 3 +#... +# GET_RN: }, +# GET_RN: "generated_by": "LNT Server v0.4.2dev" +# GET_RN: } + +lnt admin list-machines > list_machines.stdout +# RUN: FileCheck %s --check-prefix=LIST_MACHINES < %T/list_machines.stdout +# LIST_MACHINES: localhost__clang_DEV__x86_64:1 +# LIST_MACHINES-NEXT: LNT SAMPLE MACHINE:2 + +lnt admin list-runs 1 > list_runs.stdout +# RUN: FileCheck %s --check-prefix=LIST_RUNS < %T/list_runs.stdout +# LIST_RUNS: llvm_project_revision=154331 1 +# LIST_RUNS: llvm_project_revision=152289 2 + +lnt admin rm-machine 1 + +lnt admin list-machines > list_machines2.stdout +# RUN: FileCheck %s --check-prefix=LIST_MACHINES2 < %T/list_machines2.stdout +# LIST_MACHINES2-NOT: localhost__clang_DEV__x86_64:1 +# LIST_MACHINES2: LNT SAMPLE MACHINE:2 + +lnt admin rename-machine 2 hal9000 + +lnt admin list-machines > list_machines3.stdout +# RUN: FileCheck %s --check-prefix=LIST_MACHINES3 < %T/list_machines3.stdout +# LIST_MACHINES3: hal9000:2