Skip to content

Commit df7faee

Browse files
authored
Add a command to delete a test suite from a server (#162)
This will make administration of LNT instances much simpler.
1 parent 7cfbe1a commit df7faee

File tree

8 files changed

+268
-2
lines changed

8 files changed

+268
-2
lines changed

docs/api.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Complete Endpoint Summary
2222
+-------+-------------------------------------------------------+---------------------------+
2323
| POST | /schema | **Yes** |
2424
+-------+-------------------------------------------------------+---------------------------+
25+
| DELETE| /schema | **Yes** |
26+
+-------+-------------------------------------------------------+---------------------------+
2527
| GET | /machines | No |
2628
+-------+-------------------------------------------------------+---------------------------+
2729
| GET | /machines/<machine_spec> | No |
@@ -243,6 +245,37 @@ Creates or updates a test suite schema. Requires authentication.
243245
* 401 Unauthorized - Missing or invalid AuthToken
244246
* 415 Unsupported Media Type - Content-Type is not application/x-yaml
245247

248+
**DELETE** ``/api/db_<database>/v4/<testsuite>/schema``
249+
250+
Deletes the test suite schema and drops all associated tables. Requires authentication.
251+
252+
**Headers:**
253+
254+
* ``AuthToken: <token>`` (required)
255+
256+
**Example:**
257+
258+
.. code-block:: bash
259+
260+
curl --request DELETE \
261+
--header "AuthToken: SomeSecret" \
262+
http://localhost:8000/api/db_default/v4/my_suite/schema
263+
264+
**Response (200 OK):**
265+
266+
.. code-block:: json
267+
268+
{
269+
"generated_by": "LNT Server <version>",
270+
"testsuite": "my_suite"
271+
}
272+
273+
**Error Responses:**
274+
275+
* 401 Unauthorized - Missing or invalid AuthToken
276+
* 404 Not Found - Unknown test suite
277+
* 500 Internal Server Error - Failed to delete test suite or drop tables
278+
246279
Machines
247280
~~~~~~~~
248281

docs/tools.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ an authentication mechanism specified in the ``lntadmin.yaml`` file. See
9393
``lnt admin test-suite add <schema.yaml>``
9494
Add a new test suite to the server with the specified YAML schema.
9595

96+
``lnt admin test-suite delete <testsuite>``
97+
Delete the specified test suite.
98+
9699
``lnt admin rm-run <run>+``
97100
Remove the specified runs and related samples.
98101

lnt/lnttool/admin.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,30 @@ def action_test_suite_add(config, schema_file):
360360
sys.stderr.write(response.text)
361361

362362

363+
@action_test_suite.command("delete")
364+
@_pass_config
365+
@click.argument("testsuite_name", required=True)
366+
def action_test_suite_delete(config, testsuite_name):
367+
"""Delete a test suite from the server."""
368+
_check_auth_token(config)
369+
370+
url = f"{config.lnt_url}/api/db_{config.database}/v4/{testsuite_name}/schema"
371+
response = config.session.delete(url)
372+
_check_response(response)
373+
374+
try:
375+
response_data = json.loads(response.text)
376+
deleted_suite = response_data.get('testsuite')
377+
if deleted_suite:
378+
sys.stdout.write(f"{deleted_suite}\n")
379+
return
380+
if config.verbose:
381+
json.dump(response_data, sys.stderr, indent=2, sort_keys=True)
382+
except Exception:
383+
if config.verbose:
384+
sys.stderr.write(response.text)
385+
386+
363387
@click.command('create-config')
364388
def action_create_config():
365389
"""Create example configuration."""

lnt/server/db/testsuite.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,3 +534,34 @@ def sync_testsuite_with_metatables(session, testsuite):
534534
testsuite.sample_fields)
535535
testsuite = existing_ts
536536
return testsuite
537+
538+
539+
def delete_testsuite(session, name):
540+
suite = session.query(TestSuite) \
541+
.filter(TestSuite.name == name).first()
542+
if suite is None:
543+
return None
544+
545+
suite_id = suite.id
546+
session.expunge(suite)
547+
548+
session.query(TestSuiteJSONSchema) \
549+
.filter(TestSuiteJSONSchema.testsuite_name == name) \
550+
.delete(synchronize_session=False)
551+
session.query(MachineField) \
552+
.filter(MachineField.test_suite_id == suite_id) \
553+
.delete(synchronize_session=False)
554+
session.query(OrderField) \
555+
.filter(OrderField.test_suite_id == suite_id) \
556+
.delete(synchronize_session=False)
557+
session.query(RunField) \
558+
.filter(RunField.test_suite_id == suite_id) \
559+
.delete(synchronize_session=False)
560+
session.query(SampleField) \
561+
.filter(SampleField.test_suite_id == suite_id) \
562+
.delete(synchronize_session=False)
563+
564+
session.query(TestSuite) \
565+
.filter(TestSuite.id == suite_id) \
566+
.delete(synchronize_session=False)
567+
return suite

lnt/server/db/testsuitedb.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,9 @@ def __str__(self):
775775
def create_tables(self, engine):
776776
self.base.metadata.create_all(engine)
777777

778+
def drop_tables(self, engine):
779+
self.base.metadata.drop_all(engine)
780+
778781
def get_baselines(self, session):
779782
return session.query(self.Baseline).all()
780783

lnt/server/ui/api.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
from sqlalchemy.orm import joinedload
1010
from sqlalchemy.orm.exc import NoResultFound
1111

12+
from lnt.server.db import testsuite
13+
from lnt.server.db import testsuitedb
1214
from lnt.server.ui.util import convert_revision
1315
from lnt.server.ui.decorators import in_db
1416
from lnt.testing import PASS
1517
from lnt.util import logger
16-
from lnt.server.db import testsuite
17-
from lnt.server.db import testsuitedb
1818
from functools import wraps
1919

2020

@@ -428,6 +428,35 @@ def post():
428428
result['schema'] = suite.__json__()
429429
return result, 201
430430

431+
@staticmethod
432+
@requires_auth_token
433+
def delete():
434+
session = request.session
435+
suite_name = g.testsuite_name
436+
suite = session.query(testsuite.TestSuite) \
437+
.filter(testsuite.TestSuite.name == suite_name).first()
438+
if suite is None:
439+
abort(404, msg=f"Unknown test suite '{suite_name}'.")
440+
441+
tsdb = request.db.testsuite.get(suite_name)
442+
if tsdb is None:
443+
tsdb = testsuitedb.TestSuiteDB(request.db, suite_name, suite)
444+
445+
try:
446+
tsdb.drop_tables(request.db.engine)
447+
testsuite.delete_testsuite(session, suite_name)
448+
session.commit()
449+
except Exception as exc:
450+
session.rollback()
451+
abort(500, msg=f"Failed to delete test suite '{suite_name}': {exc}")
452+
453+
request.db.testsuite.pop(suite_name, None)
454+
request.db.testsuite = dict(sorted(request.db.testsuite.items()))
455+
456+
result = common_fields_factory()
457+
result['testsuite'] = suite_name
458+
return result
459+
431460

432461
class SampleData(Resource):
433462
method_decorators = [in_db]
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# RUN: rm -rf %t.instance
2+
# RUN: rm -rf %t.tmp && mkdir -p %t.tmp
3+
# RUN: python %{shared_inputs}/create_temp_instance.py \
4+
# RUN: %s %{shared_inputs}/SmallInstance %t.instance
5+
# RUN: %{shared_inputs}/server_wrapper.sh %t.instance 9096 /bin/sh %s %t.tmp %{shared_inputs}
6+
7+
set -eux
8+
DIR="$1"
9+
SHARED_INPUTS="$2"
10+
cd "$DIR"
11+
12+
cat > lntadmin.yaml << '__EOF__'
13+
lnt_url: "http://localhost:9096"
14+
database: default
15+
testsuite: delete_test_suite
16+
auth_token: test_token
17+
__EOF__
18+
19+
SCHEMA_URL="http://localhost:9096/api/db_default/v4/delete_test_suite/schema"
20+
21+
cat > schema.yaml << '__EOF__'
22+
format_version: '2'
23+
name: delete_test_suite
24+
metrics:
25+
- name: execution_time
26+
type: Real
27+
unit: seconds
28+
run_fields:
29+
- name: build_revision
30+
order: true
31+
machine_fields:
32+
- name: hardware
33+
__EOF__
34+
35+
lnt admin --testsuite delete_test_suite test-suite add schema.yaml > add_schema.stdout 2>&1
36+
# RUN: filecheck %s --check-prefix=ADD_SCHEMA < %t.tmp/add_schema.stdout
37+
# ADD_SCHEMA: delete_test_suite
38+
39+
lnt admin test-suite delete delete_test_suite > delete_schema.stdout 2>&1
40+
# RUN: filecheck %s --check-prefix=DELETE_SCHEMA < %t.tmp/delete_schema.stdout
41+
# DELETE_SCHEMA: delete_test_suite
42+
43+
curl -s -o schema_response.txt -w "%{http_code}" "$SCHEMA_URL" > schema_status.txt 2>&1
44+
# RUN: filecheck %s --check-prefix=SCHEMA_STATUS < %t.tmp/schema_status.txt
45+
# SCHEMA_STATUS: 404

tests/server/api/schema/delete.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# This test checks the /schema DELETE API that allows removing a schema.
2+
3+
# RUN: rm -rf %t.instance
4+
# RUN: python %{shared_inputs}/create_temp_instance.py \
5+
# RUN: %s %{shared_inputs}/SmallInstance \
6+
# RUN: %t.instance %S/../../ui/Inputs/V4Pages_extra_records.sql
7+
#
8+
# RUN: python %s %t.instance
9+
# END.
10+
11+
import json
12+
import logging
13+
import sys
14+
import unittest
15+
import lnt.server.ui.app
16+
17+
logging.basicConfig(level=logging.INFO)
18+
19+
20+
class SchemaDeleteApiTest(unittest.TestCase):
21+
"""Test DELETE /schema endpoint for schema removal."""
22+
23+
def setUp(self):
24+
_, instance_path = sys.argv
25+
app = lnt.server.ui.app.App.create_standalone(instance_path)
26+
app.testing = True
27+
self.client = app.test_client()
28+
29+
def _schema_payload(self, name):
30+
return f"""
31+
format_version: '2'
32+
name: {name}
33+
metrics:
34+
- name: execution_time
35+
type: Real
36+
unit: seconds
37+
run_fields:
38+
- name: build_revision
39+
order: true
40+
machine_fields:
41+
- name: hardware
42+
"""
43+
44+
def _post_schema(self, name):
45+
payload = self._schema_payload(name)
46+
return self.client.post(
47+
f"api/db_default/v4/{name}/schema",
48+
data=payload,
49+
content_type="application/x-yaml",
50+
headers={"AuthToken": "test_token"},
51+
)
52+
53+
def test_delete_requires_auth(self):
54+
resp = self._post_schema("schema_delete_suite")
55+
self.assertEqual(resp.status_code, 201, resp.data.decode("utf-8"))
56+
57+
resp = self.client.delete(
58+
"api/db_default/v4/schema_delete_suite/schema",
59+
)
60+
self.assertEqual(resp.status_code, 401)
61+
62+
def test_delete_schema_success(self):
63+
resp = self._post_schema("schema_delete_suite")
64+
self.assertEqual(resp.status_code, 201, resp.data.decode("utf-8"))
65+
66+
resp = self.client.delete(
67+
"api/db_default/v4/schema_delete_suite/schema",
68+
headers={"AuthToken": "test_token"},
69+
)
70+
self.assertEqual(resp.status_code, 200, resp.data.decode("utf-8"))
71+
result = json.loads(resp.data)
72+
self.assertEqual(result["testsuite"], "schema_delete_suite")
73+
74+
resp = self.client.get("api/db_default/v4/schema_delete_suite/schema")
75+
self.assertEqual(resp.status_code, 404)
76+
77+
def test_delete_unknown_suite(self):
78+
resp = self.client.delete(
79+
"api/db_default/v4/does_not_exist/schema",
80+
headers={"AuthToken": "test_token"},
81+
)
82+
self.assertEqual(resp.status_code, 404)
83+
84+
def test_delete_default_nts_suite(self):
85+
resp = self.client.delete(
86+
"api/db_default/v4/nts/schema",
87+
headers={"AuthToken": "test_token"},
88+
)
89+
self.assertEqual(resp.status_code, 200, resp.data.decode("utf-8"))
90+
result = json.loads(resp.data)
91+
self.assertEqual(result["testsuite"], "nts")
92+
93+
resp = self.client.get("api/db_default/v4/nts/schema")
94+
self.assertEqual(resp.status_code, 404)
95+
96+
97+
if __name__ == "__main__":
98+
unittest.main(argv=[sys.argv[0], ])

0 commit comments

Comments
 (0)