Skip to content

Commit be315c0

Browse files
Merge pull request #1303 from liambeguin/sync-places-graph
contrib: add helper to graph current configuration
2 parents a696bcb + f99c44b commit be315c0

3 files changed

Lines changed: 204 additions & 0 deletions

File tree

contrib/README.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
labgrid-webapp
2+
==============
3+
4+
labgrid-webapp implements a browser interface to access some of labgrid's
5+
information.
6+
7+
Quick Start
8+
-----------
9+
10+
.. code-block:: bash
11+
12+
$ cd labgrid/
13+
$ source venv/bin/activate
14+
venv $ pip install -r contrib/requirements-webapp.txt
15+
venv $ ./contrib/labgrid-webapp --help
16+
usage: labgrid-webapp [-h] [--crossbar URL] [--port PORT] [--proxy PROXY]
17+
18+
Labgrid webapp
19+
20+
options:
21+
-h, --help show this help message and exit
22+
--crossbar URL, -x URL
23+
Crossbar websocket URL (default: ws://127.0.0.1:20408/ws)
24+
--port PORT Port to serve on
25+
--proxy PROXY, -P PROXY
26+
27+
venv $ ./contrib/labgrid-webapp --help
28+
INFO: Available routes:
29+
INFO: - /labgrid/graph
30+
INFO: Started server process [2378028]
31+
INFO: Waiting for application startup.
32+
INFO: Application startup complete.
33+
INFO: Uvicorn running on http://0.0.0.0:8800 (Press CTRL+C to quit)
34+
...
35+
36+
Please note that the graph feature relies on a valid `graphviz` system
37+
installation.
38+
39+
By default the application will start on port 8800.
40+
41+
See http://0.0.0.0:8800/docs for more information on available endpoints.

contrib/labgrid-webapp

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import logging
4+
import os
5+
import sys
6+
from typing import Dict
7+
8+
import graphviz
9+
import uvicorn
10+
from fastapi import FastAPI
11+
from fastapi.responses import Response
12+
13+
from labgrid.remote.client import ClientSession, start_session
14+
from labgrid.remote.common import Place
15+
from labgrid.resource import Resource
16+
from labgrid.util.proxy import proxymanager
17+
18+
19+
async def do_graph(session: ClientSession) -> bytes:
20+
'''Generate a graphviz graph of the current configuration.
21+
22+
Graph displays:
23+
- all resources, grouped by groupname and exporter.
24+
- all places, with a list of tags
25+
- solid edges between places and acquired resources
26+
- dotted edges between places and unacquired resources
27+
- edges between resources and places carry the match name if any.
28+
'''
29+
def res_node_attr(name: str, resource: Resource) -> Dict[str, str]:
30+
return {
31+
'shape': 'plaintext',
32+
'label': f'''<
33+
<table bgcolor="peru">
34+
<tr>
35+
<td border="0" align="left">Resource</td>
36+
</tr>
37+
<tr>
38+
<td port="cls">{resource.cls}</td>
39+
<td port="name" bgcolor="white">{name}</td>
40+
</tr>
41+
</table>>''',
42+
}
43+
44+
def place_node_attr(name: str, place: Place) -> Dict[str, str]:
45+
acquired = ''
46+
bgcolor = 'lightblue'
47+
if place.acquired:
48+
bgcolor = 'cornflowerblue'
49+
acquired = f'<td port="user" border="0" align="right"><b>{place.acquired}</b></td>'
50+
51+
tags = '<tr><td border="0" align="left">Tags</td></tr>' if place.tags else ''
52+
for k, v in place.tags.items():
53+
tags += f'<tr><td border="0"></td><td border="0" align="left">{k}={v}</td></tr>'
54+
55+
return {
56+
'shape': 'plaintext',
57+
'label': f'''<
58+
<table bgcolor="{bgcolor}">
59+
<tr>
60+
<td border="0" align="left">Place</td>
61+
{acquired}
62+
</tr>
63+
<tr>
64+
<td port="name" colspan="2" bgcolor="white">{name}</td>
65+
</tr>
66+
{tags}
67+
</table>>''',
68+
}
69+
70+
g = graphviz.Digraph('G')
71+
g.attr(rankdir='LR')
72+
73+
paths = {}
74+
for exporter, groups in session.resources.items():
75+
g_exporter = graphviz.Digraph(f'cluster_{exporter}')
76+
g_exporter.attr(label=exporter)
77+
78+
for group, resources in groups.items():
79+
g_group = graphviz.Digraph(f'cluster_{group}')
80+
g_group.attr(label=group)
81+
82+
for r_name, entry in resources.items():
83+
res_node = f'{exporter}/{group}/{entry.cls}/{r_name}'.replace(':', '_')
84+
paths[res_node] = [exporter, group, entry.cls, r_name]
85+
g_group.node(res_node, **res_node_attr(r_name, entry))
86+
87+
g_exporter.subgraph(g_group)
88+
89+
g.subgraph(g_exporter)
90+
91+
for p_node, place in session.places.items():
92+
g.node(p_node, **place_node_attr(p_node, place))
93+
94+
for m in place.matches:
95+
for node, p in paths.items():
96+
if m.ismatch(p):
97+
g.edge(
98+
f'{node}:name', p_node,
99+
style='solid' if place.acquired else 'dotted',
100+
label=m.rename if m.rename else None,
101+
)
102+
103+
return g.pipe(format='svg')
104+
105+
106+
def main():
107+
app = FastAPI()
108+
logger = logging.getLogger('uvicorn')
109+
110+
@app.get('/labgrid/graph')
111+
async def get_graph() -> str:
112+
'''Show a graph of the current infrastructure.'''
113+
svg = await do_graph(session)
114+
return Response(content=svg, media_type='image/svg+xml')
115+
116+
parser = argparse.ArgumentParser(
117+
description='Labgrid webapp',
118+
formatter_class=argparse.RawDescriptionHelpFormatter,
119+
)
120+
parser.add_argument(
121+
'--crossbar',
122+
'-x',
123+
metavar='URL',
124+
default=os.environ.get('LG_CROSSBAR', 'ws://127.0.0.1:20408/ws'),
125+
help='Crossbar websocket URL (default: %(default)s)',
126+
)
127+
parser.add_argument('--port', type=int, default=8800, help='Port to serve on')
128+
parser.add_argument('--proxy', '-P', help='Proxy connections via given ssh host')
129+
130+
args = parser.parse_args()
131+
132+
if args.proxy:
133+
proxymanager.force_proxy(args.proxy)
134+
135+
try:
136+
session = start_session(
137+
args.crossbar, os.environ.get('LG_CROSSBAR_REALM', 'realm1'), {},
138+
)
139+
except ConnectionRefusedError:
140+
logger.fatal('Unable to connect to labgrid crossbar')
141+
return
142+
143+
server = uvicorn.Server(config=uvicorn.Config(
144+
loop=session.loop,
145+
host='0.0.0.0',
146+
port=args.port,
147+
app=app,
148+
))
149+
150+
logger.info('Available routes:')
151+
for route in app.routes:
152+
reserved_routes = ['/openapi.json', '/docs', '/docs/oauth2-redirect', '/redoc']
153+
if route.path not in reserved_routes:
154+
logger.info(f' - {route.path}')
155+
156+
session.loop.run_until_complete(server.serve())
157+
158+
159+
if __name__ == '__main__':
160+
sys.exit(main())

contrib/requirements-webapp.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fastapi
2+
graphviz
3+
uvicorn

0 commit comments

Comments
 (0)