[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