diff --git a/deploy/README.md b/deploy/README.md index c368945..25ebe42 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,4 +1,4 @@ -# Connection-service +# connectivityserver This service provides a very simple flask based server to serve connection information to DAQ applications. @@ -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 diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh index 430a0f3..c808bbe 100755 --- a/deploy/entrypoint.sh +++ b/deploy/entrypoint.sh @@ -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 diff --git a/docs/README.md b/docs/README.md index 7e281e2..8028c69 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -# Connection-service +# connectivityserver This service provides a very simple flask based server to serve connection information to DAQ applications. @@ -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 diff --git a/gunicorn.conf.py b/gunicorn.conf.py index 0e8c7f9..724c6e1 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -3,4 +3,4 @@ worker_class="gthread" threads=2 timeout=5000000000 -wsgi_app="connection-service.connection-flask:app" +wsgi_app="connectivityserver.connectionflask:app" diff --git a/pyproject.toml b/pyproject.toml index 50797d2..fb53a2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/src/connection-service/__init__.py b/src/connectivityserver/__init__.py similarity index 100% rename from src/connection-service/__init__.py rename to src/connectivityserver/__init__.py diff --git a/src/connection-service/connection-flask.py b/src/connectivityserver/connectionflask.py similarity index 90% rename from src/connection-service/connection-flask.py rename to src/connectivityserver/connectionflask.py index 561980f..f723816 100755 --- a/src/connection-service/connection-flask.py +++ b/src/connectivityserver/connectionflask.py @@ -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() @@ -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 @@ -55,8 +59,8 @@ def convert_log_level(log_level): def dump(): now=datetime.now() dstream=StringIO() - dstream.write(f'

Dump of configuration dictionary

') - dstream.write(f"

Active partitions

") + dstream.write('

Dump of configuration dictionary

') + dstream.write("

Active partitions

") if len(partitions)>0: pad=' style="padding-left: 1em;padding-right: 1em"' dstream.write(f'' @@ -65,7 +69,7 @@ def dump(): for p in partitions: dstream.write(f'{p}' f'{len(partitions[p])}') - dstream.write(f"
") + dstream.write("") for p in partitions: store=partitions[p] dstream.write(f'

Partition {p}

') @@ -76,8 +80,8 @@ def dump(): dstream.write(f'{k}: {v}
') dstream.write("

") else: - dstream.write(f"None

") - dstream.write(f"

Server statistics

") + dstream.write("None

") + dstream.write("

Server statistics

") stats_to_html(dstream) dstream.seek(0) return dstream.read() @@ -103,7 +107,7 @@ def stats_to_html(dstream): @app.route("/stats") def dumpStats(): dstream=StringIO() - dstream.write(f'

Connection server statistics

') + dstream.write('

Connection server statistics

') stats_to_html(dstream) dstream.seek(0) return dstream.read() @@ -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( @@ -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}]") @@ -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'] @@ -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: @@ -237,11 +246,13 @@ def retract(): partlock.release() if good: return 'OK' - else: - abort(404) + abort(404) @app.route("/getconnection/",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. @@ -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) diff --git a/tests/test_connectivityservice.py b/tests/test_connectivityservice.py new file mode 100644 index 0000000..ec31065 --- /dev/null +++ b/tests/test_connectivityservice.py @@ -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"

Connection server statistics" in resp.data + assert b"

0 calls to publish" not in resp.data + + resp = client.get("/resetStats") + assert resp.status_code == 200 + assert b"

Connection server statistics" in resp.data + assert b"

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

Connection server statistics" in resp.data + assert b"

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"

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"

0 calls to publish" in resp.data