diff --git a/contrib/README.rst b/contrib/README.rst new file mode 100644 index 000000000..30e77c078 --- /dev/null +++ b/contrib/README.rst @@ -0,0 +1,41 @@ +labgrid-webapp +============== + +labgrid-webapp implements a browser interface to access some of labgrid's +information. + +Quick Start +----------- + +.. code-block:: bash + + $ cd labgrid/ + $ source venv/bin/activate + venv $ pip install -r contrib/requirements-webapp.txt + venv $ ./contrib/labgrid-webapp --help + usage: labgrid-webapp [-h] [--crossbar URL] [--port PORT] [--proxy PROXY] + + Labgrid webapp + + options: + -h, --help show this help message and exit + --crossbar URL, -x URL + Crossbar websocket URL (default: ws://127.0.0.1:20408/ws) + --port PORT Port to serve on + --proxy PROXY, -P PROXY + + venv $ ./contrib/labgrid-webapp --help + INFO: Available routes: + INFO: - /labgrid/graph + INFO: Started server process [2378028] + INFO: Waiting for application startup. + INFO: Application startup complete. + INFO: Uvicorn running on http://0.0.0.0:8800 (Press CTRL+C to quit) + ... + +Please note that the graph feature relies on a valid `graphviz` system +installation. + +By default the application will start on port 8800. + +See http://0.0.0.0:8800/docs for more information on available endpoints. diff --git a/contrib/labgrid-webapp b/contrib/labgrid-webapp new file mode 100755 index 000000000..bd4a22178 --- /dev/null +++ b/contrib/labgrid-webapp @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +import argparse +import logging +import os +import sys +from typing import Dict + +import graphviz +import uvicorn +from fastapi import FastAPI +from fastapi.responses import Response + +from labgrid.remote.client import ClientSession, start_session +from labgrid.remote.common import Place +from labgrid.resource import Resource +from labgrid.util.proxy import proxymanager + + +async def do_graph(session: ClientSession) -> bytes: + '''Generate a graphviz graph of the current configuration. + + Graph displays: + - all resources, grouped by groupname and exporter. + - all places, with a list of tags + - solid edges between places and acquired resources + - dotted edges between places and unacquired resources + - edges between resources and places carry the match name if any. + ''' + def res_node_attr(name: str, resource: Resource) -> Dict[str, str]: + return { + 'shape': 'plaintext', + 'label': f'''< + + + + + + + + +
Resource
{resource.cls}{name}
>''', + } + + def place_node_attr(name: str, place: Place) -> Dict[str, str]: + acquired = '' + bgcolor = 'lightblue' + if place.acquired: + bgcolor = 'cornflowerblue' + acquired = f'{place.acquired}' + + tags = 'Tags' if place.tags else '' + for k, v in place.tags.items(): + tags += f'{k}={v}' + + return { + 'shape': 'plaintext', + 'label': f'''< + + + + {acquired} + + + + + {tags} +
Place
{name}
>''', + } + + g = graphviz.Digraph('G') + g.attr(rankdir='LR') + + paths = {} + for exporter, groups in session.resources.items(): + g_exporter = graphviz.Digraph(f'cluster_{exporter}') + g_exporter.attr(label=exporter) + + for group, resources in groups.items(): + g_group = graphviz.Digraph(f'cluster_{group}') + g_group.attr(label=group) + + for r_name, entry in resources.items(): + res_node = f'{exporter}/{group}/{entry.cls}/{r_name}'.replace(':', '_') + paths[res_node] = [exporter, group, entry.cls, r_name] + g_group.node(res_node, **res_node_attr(r_name, entry)) + + g_exporter.subgraph(g_group) + + g.subgraph(g_exporter) + + for p_node, place in session.places.items(): + g.node(p_node, **place_node_attr(p_node, place)) + + for m in place.matches: + for node, p in paths.items(): + if m.ismatch(p): + g.edge( + f'{node}:name', p_node, + style='solid' if place.acquired else 'dotted', + label=m.rename if m.rename else None, + ) + + return g.pipe(format='svg') + + +def main(): + app = FastAPI() + logger = logging.getLogger('uvicorn') + + @app.get('/labgrid/graph') + async def get_graph() -> str: + '''Show a graph of the current infrastructure.''' + svg = await do_graph(session) + return Response(content=svg, media_type='image/svg+xml') + + parser = argparse.ArgumentParser( + description='Labgrid webapp', + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + '--crossbar', + '-x', + metavar='URL', + default=os.environ.get('LG_CROSSBAR', 'ws://127.0.0.1:20408/ws'), + help='Crossbar websocket URL (default: %(default)s)', + ) + parser.add_argument('--port', type=int, default=8800, help='Port to serve on') + parser.add_argument('--proxy', '-P', help='Proxy connections via given ssh host') + + args = parser.parse_args() + + if args.proxy: + proxymanager.force_proxy(args.proxy) + + try: + session = start_session( + args.crossbar, os.environ.get('LG_CROSSBAR_REALM', 'realm1'), {}, + ) + except ConnectionRefusedError: + logger.fatal('Unable to connect to labgrid crossbar') + return + + server = uvicorn.Server(config=uvicorn.Config( + loop=session.loop, + host='0.0.0.0', + port=args.port, + app=app, + )) + + logger.info('Available routes:') + for route in app.routes: + reserved_routes = ['/openapi.json', '/docs', '/docs/oauth2-redirect', '/redoc'] + if route.path not in reserved_routes: + logger.info(f' - {route.path}') + + session.loop.run_until_complete(server.serve()) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/contrib/requirements-webapp.txt b/contrib/requirements-webapp.txt new file mode 100644 index 000000000..539b76d5e --- /dev/null +++ b/contrib/requirements-webapp.txt @@ -0,0 +1,3 @@ +fastapi +graphviz +uvicorn