Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions deploy/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Connection-service
# connectivityserver

This service provides a very simple flask based
server to serve connection information to DAQ applications.
Expand All @@ -11,7 +11,7 @@ docker buildx build --tag ghcr.io/dune-daq/connectivityserver:latest .
```
Or, if you want to specify a tag
```bash
docker buildx build --tag ghcr.io/dune-daq/connectivityserver:v1.2.0 --build-arg VERSION=v1.2.0 .
docker buildx build --tag ghcr.io/dune-daq/connectivityserver:v1.3.0 --build-arg VERSION=v1.3.0 .
```

Apply the kubernetes manifest from connectivityserver.yaml. This
Expand Down
2 changes: 1 addition & 1 deletion deploy/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/bash

exec gunicorn -b 0.0.0.0:5000 --workers=1 --worker-class=gthread --threads=2 --timeout 5000000000 --log-level=debug connection-service.connection-flask:app
exec gunicorn -b 0.0.0.0:5000 --workers=1 --worker-class=gthread --threads=2 --timeout 5000000000 --log-level=debug connectivityserver.connectionflask:app
6 changes: 3 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Connection-service
# connectivityserver

This service provides a very simple flask based
server to serve connection information to DAQ applications.
Expand Down Expand Up @@ -55,14 +55,14 @@ This uri should be used to remove published connections. The request should be J

### /retract-partition
This uri should be used to remove all published connections from the
given partition. The request should be a urlencoded form with one field "partition" naming the partition to be retracted.
given partition. The request should be JSON encoded with one field "partition" naming the partition to be retracted.

## Running the server locally from the command line
The server is intended to be run under the Gunicorn web server.

```
gunicorn -b 0.0.0.0:5000 --workers=1 --worker-class=gthread --threads=2 \
--timeout 5000000000 connection-service.connection-flask:app
--timeout 5000000000 connectivityserver.connectionflask:app
```

Some debug information will be printed by the connection-flask if the
Expand Down
2 changes: 1 addition & 1 deletion gunicorn.conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
worker_class="gthread"
threads=2
timeout=5000000000
wsgi_app="connection-service.connection-flask:app"
wsgi_app="connectivityserver.connectionflask:app"
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ build-backend = "setuptools.build_meta"
requires = ["setuptools>=61.0"]

[project]
name = "connection-service"
name = "connectivityserver"
description = "A very simple Flask based server to serve connection information to DAQ applications"
version = "1.2.0"
version = "1.3.0"
readme = "docs/README.md"
requires-python = ">=3.6"
urls = { "homepage" = "https://github.com/DUNE-DAQ/connectivityserver" }
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
# received with this code.
#

import os
import json
import logging
import os
import re
from threading import Lock
from io import StringIO
from datetime import datetime, timedelta
from collections import namedtuple
import logging
from flask import Flask, request, abort, make_response
from datetime import datetime, timedelta
from io import StringIO
from threading import Lock

from flask import Flask, abort, make_response, request

# Some functions exit with an abort(NNN) instead of return so don't complain!
#ruff: noqa RET503

partitions={}
partlock=Lock()
Expand All @@ -26,9 +30,9 @@
def convert_log_level(log_level):
if log_level == 0:
return logging.WARNING
elif log_level == 1:
if log_level == 1:
return logging.INFO
elif log_level == 2:
if log_level == 2:
return logging.DEBUG
return logging.INFO

Expand All @@ -55,8 +59,8 @@ def convert_log_level(log_level):
def dump():
now=datetime.now()
dstream=StringIO()
dstream.write(f'<h1>Dump of configuration dictionary</h1>')
dstream.write(f"<h2>Active partitions</h2><p>")
dstream.write('<h1>Dump of configuration dictionary</h1>')
dstream.write("<h2>Active partitions</h2><p>")
if len(partitions)>0:
pad=' style="padding-left: 1em;padding-right: 1em"'
dstream.write(f'<table style="border: 1px solid black">'
Expand All @@ -65,7 +69,7 @@ def dump():
for p in partitions:
dstream.write(f'<tr><td{pad}>{p}'
f'</td><td{pad}>{len(partitions[p])}</td></tr>')
dstream.write(f"</table>")
dstream.write("</table>")
for p in partitions:
store=partitions[p]
dstream.write(f'<h2>Partition {p}</h2><p>')
Expand All @@ -76,8 +80,8 @@ def dump():
dstream.write(f'<strike>{k}: {v}</strike></br>')
dstream.write("</p>")
else:
dstream.write(f"None</p>")
dstream.write(f"<hr><h2>Server statistics</h2>")
dstream.write("None</p>")
dstream.write("<hr><h2>Server statistics</h2>")
stats_to_html(dstream)
dstream.seek(0)
return dstream.read()
Expand All @@ -103,7 +107,7 @@ def stats_to_html(dstream):
@app.route("/stats")
def dumpStats():
dstream=StringIO()
dstream.write(f'<h1>Connection server statistics</h1>')
dstream.write('<h1>Connection server statistics</h1>')
stats_to_html(dstream)
dstream.seek(0)
return dstream.read()
Expand Down Expand Up @@ -155,7 +159,7 @@ def publish():
global maxpartitions
if len(partitions)>maxpartitions:
maxpartitions=len(partitions)
if not part in maxentries:
if part not in maxentries:
maxentries[part]=0

Connection=namedtuple(
Expand Down Expand Up @@ -193,7 +197,8 @@ def publish():

@app.route("/retract-partition",methods=['POST'])
def retract_partition():

if len(request.data) == 0:
abort(400)

js=json.loads(request.data)
log.debug(f"request=[{js}]")
Expand All @@ -208,12 +213,14 @@ def retract_partition():
partitions.pop(part)
partlock.release()
return 'OK'
else:
partlock.release()
abort(404)
partlock.release()
abort(404)

@app.route("/retract",methods=['POST'])
def retract():
if len(request.data) == 0:
abort(400)

js=json.loads(request.data)
good=True
part=js['partition']
Expand All @@ -223,6 +230,8 @@ def retract():
return make_response(f"Partition {part} not found", 404)

store=partitions[part]
if 'connections' not in js:
abort(400)
for con in js['connections']:
id=con['connection_id']
if id in store:
Expand All @@ -237,11 +246,13 @@ def retract():
partlock.release()
if good:
return 'OK'
else:
abort(404)
abort(404)

@app.route("/getconnection/<part>",methods=['POST','GET'])
def get_connection(part):
if len(request.data) == 0:
abort(400)

# Find connection uris that correspond to the connection id pattern
# in the request. The pattern is treated as a regular expression.

Expand Down Expand Up @@ -288,10 +299,10 @@ def get_connection(part):
lookup_time+=td

return "["+",".join(result)+"]"
else:
partlock.release()
log.info(f"Partition {part} not found")
abort(404)

partlock.release()
log.info(f"Partition {part} not found")
abort(404)
else:
abort(400)

137 changes: 137 additions & 0 deletions tests/test_connectivityservice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import json
import pytest

import connectivityserver.connectionflask as cf

@pytest.fixture()
def app():
yield cf.app


@pytest.fixture()
def client(app):
return app.test_client()

@pytest.fixture()
def runner(app):
return app.test_cli_runner()




con = json.loads("""{
"connections":[
{
"connection_type":0,
"data_type":"TPSet",
"uid":"DRO-000-tp_to_trigger",
"uri":"tcp://192.168.1.100:1234"
},
{
"connection_type":0,
"data_type":"TPSet",
"uid":"DRO-001-tp_to_trigger",
"uri":"tcp://192.168.1.100:1235"
}
],
"partition":"ccTest"
}""")



def test_noconnection(client):
resp = client.get("/getconnection/bad/con")
assert resp.status_code == 404

def test_publish(client):
resp = client.post("/publish", json=con)
assert resp.status_code == 200

def test_lookup(client):
query = json.loads("""{"uid_regex":"DRO.*", "data_type":"TPSet"}""")
resp = client.post("/getconnection/ccTest", json=query)
assert resp.status_code == 200

rjson = json.loads(resp.data)
assert len(rjson) == 2
assert rjson[0]["uid"] == "DRO-000-tp_to_trigger"
assert rjson[1]["uid"] == "DRO-001-tp_to_trigger"

query = json.loads("""{"uid_regex":"DUMMY.*", "data_type":"TPSet"}""")
resp = client.post("/getconnection/ccTest", json=query)
assert resp.status_code == 200
rjson = json.loads(resp.data)
assert len(rjson) == 0


def test_retract(client):
resp = client.post("/retract")
assert resp.status_code == 400

retraction = json.loads("""{"partition":"ccTest",
"connections":[{"connection_id":"DRO-000-tp_to_trigger"},
{"connection_id":"DRO-001-tp_to_trigger"}]
}""")
resp = client.post("/retract", json=retraction)
assert resp.status_code == 200

query = json.loads("""{"uid_regex":"DRO.*", "data_type":"TPSet"}""")
resp = client.post("/getconnection/ccTest", json=query)
assert resp.status_code == 404

# Second time should fail
resp = client.post("/retract", json=retraction)
assert resp.status_code == 404


def test_retract_partition(client):
resp = client.post("/publish", json=con)
assert resp.status_code == 200

resp = client.post("/retract-partition")
assert resp.status_code == 400

retraction = json.loads("""{"partition":"ccTest"}""")
resp = client.post("/retract-partition", json=retraction)
assert resp.status_code == 200

# Second time should fail
resp = client.post("/retract-partition", json=retraction)
assert resp.status_code == 404


def test_dump(client):
resp = client.get("/")
assert b"Dump" in resp.data

def test_stats(client):
resp = client.get("/stats")
assert resp.status_code == 200
assert b"<h1>Connection server statistics" in resp.data
assert b"<p>0 calls to publish" not in resp.data

resp = client.get("/resetStats")
assert resp.status_code == 200
assert b"<h1>Connection server statistics" in resp.data
assert b"<p>0 calls to publish" not in resp.data

resp = client.get("/stats")
assert resp.status_code == 200
assert b"<h1>Connection server statistics" in resp.data
assert b"<p>0 calls to publish" in resp.data


def test_reset(client):
resp = client.post("/publish", json=con)
assert resp.status_code == 200

resp = client.get("/stats")
assert resp.status_code == 200
assert b"<p>0 calls to publish" not in resp.data

resp = client.get("/resetService")
assert resp.status_code == 200

resp = client.get("/stats")
assert resp.status_code == 200
assert b"<p>0 calls to publish" in resp.data