[LNT] r308989 - Add lnt commandline REST client
Matthias Braun via llvm-commits
llvm-commits at lists.llvm.org
Tue Jul 25 10:16:33 PDT 2017
Author: matze
Date: Tue Jul 25 10:16:33 2017
New Revision: 308989
URL: http://llvm.org/viewvc/llvm-project?rev=308989&view=rev
Log:
Add lnt commandline REST client
Adds `lnt admin` subcommands that allow manipulation of the data in an
LNT database. It currently supports:
- create-config Create example configuration.
- get-machine Download machine information and run list.
- get-run Download runs and save as report files.
- list-machines List machines and their id numbers.
- list-runs List runs of a machine.
- merge-machine-into Merge machine into another machine.
- post-run Submit report files to server.
- rename-machine Rename machine.
- rm-machine Remove machine and related data.
- rm-run Remove runs and related data.
Differential Revision: https://reviews.llvm.org/D35501
Added:
lnt/trunk/lnt/lnttool/admin.py
lnt/trunk/tests/lnttool/admin.shtest
Modified:
lnt/trunk/docs/api.rst
lnt/trunk/docs/tools.rst
lnt/trunk/lnt/lnttool/main.py
Modified: lnt/trunk/docs/api.rst
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/docs/api.rst?rev=308989&r1=308988&r2=308989&view=diff
==============================================================================
--- lnt/trunk/docs/api.rst (original)
+++ lnt/trunk/docs/api.rst Tue Jul 25 10:16:33 2017
@@ -37,6 +37,8 @@ once.
| /samples/`id` | Get all non-empty sample info for Sample `id`. |
+---------------------------+------------------------------------------------------------------------------------------+
+.. _auth_tokens:
+
Write Operations
----------------
Modified: lnt/trunk/docs/tools.rst
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/docs/tools.rst?rev=308989&r1=308988&r2=308989&view=diff
==============================================================================
--- lnt/trunk/docs/tools.rst (original)
+++ lnt/trunk/docs/tools.rst Tue Jul 25 10:16:33 2017
@@ -44,6 +44,49 @@ Client-Side Tools
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 <machine>``
+ Download machine information and run list for a specific machine.
+
+ ``lnt admin rm-machine <machine>``
+ Removes the specified machine and related runs and samples.
+
+ ``lnt admin rename-machine <machine> <new-name>``
+ Renames the specified machine.
+
+ ``lnt admin merge-machine-into <machine> <merge-into-machine>``
+ Move all runs from one machine to another machine and delete the machine.
+
+ ``lnt admin list-runs <machine>``
+ List all runs for the specified machine.
+
+ ``lnt admin get-run <run>+``
+ Download the specified runs.
+
+ ``lnt admin post-run <filename>+``
+ Post the specified report files as a new runs to the server.
+
+ ``lnt admin rm-run <run>+``
+ Remove the specified runs and related samples.
+
+
Server-Side Tools
-----------------
Added: lnt/trunk/lnt/lnttool/admin.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/lnttool/admin.py?rev=308989&view=auto
==============================================================================
--- lnt/trunk/lnt/lnttool/admin.py (added)
+++ lnt/trunk/lnt/lnttool/admin.py Tue Jul 25 10:16:33 2017
@@ -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
+
+
+ at click.command("list-machines")
+ at _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')
+
+
+ at click.command("get-machine")
+ at click.argument("machine")
+ at _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)
+
+
+ at click.command("rm-machine")
+ at click.argument("machine")
+ at _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()
+
+
+ at click.command("rename-machine")
+ at click.argument("machine")
+ at click.argument("new-name")
+ at _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)
+
+
+ at click.command("merge-machine-into")
+ at click.argument("machine")
+ at click.argument("into")
+ at _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)
+
+
+ at click.command("list-runs")
+ at click.argument("machine")
+ at _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')
+
+
+ at click.command("get-run")
+ at click.argument("runs", nargs=-1, required=True)
+ at _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)
+
+
+ at click.command("rm-run")
+ at click.argument("runs", nargs=-1, required=True)
+ at _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)
+
+
+ at click.command("post-run")
+ at click.argument("datafiles", nargs=-1, type=click.Path(exists=True),
+ required=True)
+ at _common_options
+ at click.option("--update-machine", is_flag=True, help="Update machine fields")
+ at 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')
+
+
+ at 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)
+
+
+ at click.group("admin", cls=AdminCLI, no_args_is_help=True)
+def group_admin():
+ """LNT server admin client."""
Modified: lnt/trunk/lnt/lnttool/main.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/lnttool/main.py?rev=308989&r1=308988&r2=308989&view=diff
==============================================================================
--- lnt/trunk/lnt/lnttool/main.py (original)
+++ lnt/trunk/lnt/lnttool/main.py Tue Jul 25 10:16:33 2017
@@ -6,6 +6,7 @@ from .import_data import action_import
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 @@ class RunTestCLI(click.MultiCommand):
@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 @@ def cli():
Use ``lnt <command> --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_submit)
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():
Added: lnt/trunk/tests/lnttool/admin.shtest
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/tests/lnttool/admin.shtest?rev=308989&view=auto
==============================================================================
--- lnt/trunk/tests/lnttool/admin.shtest (added)
+++ lnt/trunk/tests/lnttool/admin.shtest Tue Jul 25 10:16:33 2017
@@ -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
More information about the llvm-commits
mailing list