Add a command to delete a test suite from a server (#162) This will make administration of LNT instances much simpler.
diff --git a/docs/api.rst b/docs/api.rst index 4bb49ec..e86cedd 100644 --- a/docs/api.rst +++ b/docs/api.rst
@@ -22,6 +22,8 @@ +-------+-------------------------------------------------------+---------------------------+ | POST | /schema | **Yes** | +-------+-------------------------------------------------------+---------------------------+ +| DELETE| /schema | **Yes** | ++-------+-------------------------------------------------------+---------------------------+ | GET | /machines | No | +-------+-------------------------------------------------------+---------------------------+ | GET | /machines/<machine_spec> | No | @@ -243,6 +245,37 @@ * 401 Unauthorized - Missing or invalid AuthToken * 415 Unsupported Media Type - Content-Type is not application/x-yaml +**DELETE** ``/api/db_<database>/v4/<testsuite>/schema`` + +Deletes the test suite schema and drops all associated tables. Requires authentication. + +**Headers:** + +* ``AuthToken: <token>`` (required) + +**Example:** + +.. code-block:: bash + + curl --request DELETE \ + --header "AuthToken: SomeSecret" \ + http://localhost:8000/api/db_default/v4/my_suite/schema + +**Response (200 OK):** + +.. code-block:: json + + { + "generated_by": "LNT Server <version>", + "testsuite": "my_suite" + } + +**Error Responses:** + +* 401 Unauthorized - Missing or invalid AuthToken +* 404 Not Found - Unknown test suite +* 500 Internal Server Error - Failed to delete test suite or drop tables + Machines ~~~~~~~~
diff --git a/docs/tools.rst b/docs/tools.rst index f88695f..4b5a8dc 100644 --- a/docs/tools.rst +++ b/docs/tools.rst
@@ -93,6 +93,9 @@ ``lnt admin test-suite add <schema.yaml>`` Add a new test suite to the server with the specified YAML schema. + ``lnt admin test-suite delete <testsuite>`` + Delete the specified test suite. + ``lnt admin rm-run <run>+`` Remove the specified runs and related samples.
diff --git a/lnt/lnttool/admin.py b/lnt/lnttool/admin.py index a943421..41272ee 100644 --- a/lnt/lnttool/admin.py +++ b/lnt/lnttool/admin.py
@@ -360,6 +360,30 @@ sys.stderr.write(response.text) +@action_test_suite.command("delete") +@_pass_config +@click.argument("testsuite_name", required=True) +def action_test_suite_delete(config, testsuite_name): + """Delete a test suite from the server.""" + _check_auth_token(config) + + url = f"{config.lnt_url}/api/db_{config.database}/v4/{testsuite_name}/schema" + response = config.session.delete(url) + _check_response(response) + + try: + response_data = json.loads(response.text) + deleted_suite = response_data.get('testsuite') + if deleted_suite: + sys.stdout.write(f"{deleted_suite}\n") + return + if config.verbose: + json.dump(response_data, sys.stderr, indent=2, sort_keys=True) + except Exception: + if config.verbose: + sys.stderr.write(response.text) + + @click.command('create-config') def action_create_config(): """Create example configuration."""
diff --git a/lnt/server/db/testsuite.py b/lnt/server/db/testsuite.py index af88d41..7ee0154 100644 --- a/lnt/server/db/testsuite.py +++ b/lnt/server/db/testsuite.py
@@ -534,3 +534,34 @@ testsuite.sample_fields) testsuite = existing_ts return testsuite + + +def delete_testsuite(session, name): + suite = session.query(TestSuite) \ + .filter(TestSuite.name == name).first() + if suite is None: + return None + + suite_id = suite.id + session.expunge(suite) + + session.query(TestSuiteJSONSchema) \ + .filter(TestSuiteJSONSchema.testsuite_name == name) \ + .delete(synchronize_session=False) + session.query(MachineField) \ + .filter(MachineField.test_suite_id == suite_id) \ + .delete(synchronize_session=False) + session.query(OrderField) \ + .filter(OrderField.test_suite_id == suite_id) \ + .delete(synchronize_session=False) + session.query(RunField) \ + .filter(RunField.test_suite_id == suite_id) \ + .delete(synchronize_session=False) + session.query(SampleField) \ + .filter(SampleField.test_suite_id == suite_id) \ + .delete(synchronize_session=False) + + session.query(TestSuite) \ + .filter(TestSuite.id == suite_id) \ + .delete(synchronize_session=False) + return suite
diff --git a/lnt/server/db/testsuitedb.py b/lnt/server/db/testsuitedb.py index 3dc6419..81e0d4b 100644 --- a/lnt/server/db/testsuitedb.py +++ b/lnt/server/db/testsuitedb.py
@@ -775,6 +775,9 @@ def create_tables(self, engine): self.base.metadata.create_all(engine) + def drop_tables(self, engine): + self.base.metadata.drop_all(engine) + def get_baselines(self, session): return session.query(self.Baseline).all()
diff --git a/lnt/server/ui/api.py b/lnt/server/ui/api.py index 00119c9..b4d3ffe 100644 --- a/lnt/server/ui/api.py +++ b/lnt/server/ui/api.py
@@ -9,12 +9,12 @@ from sqlalchemy.orm import joinedload from sqlalchemy.orm.exc import NoResultFound +from lnt.server.db import testsuite +from lnt.server.db import testsuitedb from lnt.server.ui.util import convert_revision from lnt.server.ui.decorators import in_db from lnt.testing import PASS from lnt.util import logger -from lnt.server.db import testsuite -from lnt.server.db import testsuitedb from functools import wraps @@ -428,6 +428,35 @@ result['schema'] = suite.__json__() return result, 201 + @staticmethod + @requires_auth_token + def delete(): + session = request.session + suite_name = g.testsuite_name + suite = session.query(testsuite.TestSuite) \ + .filter(testsuite.TestSuite.name == suite_name).first() + if suite is None: + abort(404, msg=f"Unknown test suite '{suite_name}'.") + + tsdb = request.db.testsuite.get(suite_name) + if tsdb is None: + tsdb = testsuitedb.TestSuiteDB(request.db, suite_name, suite) + + try: + tsdb.drop_tables(request.db.engine) + testsuite.delete_testsuite(session, suite_name) + session.commit() + except Exception as exc: + session.rollback() + abort(500, msg=f"Failed to delete test suite '{suite_name}': {exc}") + + request.db.testsuite.pop(suite_name, None) + request.db.testsuite = dict(sorted(request.db.testsuite.items())) + + result = common_fields_factory() + result['testsuite'] = suite_name + return result + class SampleData(Resource): method_decorators = [in_db]
diff --git a/tests/lnttool/admin/test-suite/delete.shtest b/tests/lnttool/admin/test-suite/delete.shtest new file mode 100644 index 0000000..48ea879 --- /dev/null +++ b/tests/lnttool/admin/test-suite/delete.shtest
@@ -0,0 +1,45 @@ +# RUN: rm -rf %t.instance +# RUN: rm -rf %t.tmp && mkdir -p %t.tmp +# RUN: python %{shared_inputs}/create_temp_instance.py \ +# RUN: %s %{shared_inputs}/SmallInstance %t.instance +# RUN: %{shared_inputs}/server_wrapper.sh %t.instance 9096 /bin/sh %s %t.tmp %{shared_inputs} + +set -eux +DIR="$1" +SHARED_INPUTS="$2" +cd "$DIR" + +cat > lntadmin.yaml << '__EOF__' +lnt_url: "http://localhost:9096" +database: default +testsuite: delete_test_suite +auth_token: test_token +__EOF__ + +SCHEMA_URL="http://localhost:9096/api/db_default/v4/delete_test_suite/schema" + +cat > schema.yaml << '__EOF__' +format_version: '2' +name: delete_test_suite +metrics: + - name: execution_time + type: Real + unit: seconds +run_fields: + - name: build_revision + order: true +machine_fields: + - name: hardware +__EOF__ + +lnt admin --testsuite delete_test_suite test-suite add schema.yaml > add_schema.stdout 2>&1 +# RUN: filecheck %s --check-prefix=ADD_SCHEMA < %t.tmp/add_schema.stdout +# ADD_SCHEMA: delete_test_suite + +lnt admin test-suite delete delete_test_suite > delete_schema.stdout 2>&1 +# RUN: filecheck %s --check-prefix=DELETE_SCHEMA < %t.tmp/delete_schema.stdout +# DELETE_SCHEMA: delete_test_suite + +curl -s -o schema_response.txt -w "%{http_code}" "$SCHEMA_URL" > schema_status.txt 2>&1 +# RUN: filecheck %s --check-prefix=SCHEMA_STATUS < %t.tmp/schema_status.txt +# SCHEMA_STATUS: 404
diff --git a/tests/server/api/schema/delete.py b/tests/server/api/schema/delete.py new file mode 100644 index 0000000..c0f34f3 --- /dev/null +++ b/tests/server/api/schema/delete.py
@@ -0,0 +1,98 @@ +# This test checks the /schema DELETE API that allows removing a schema. + +# RUN: rm -rf %t.instance +# RUN: python %{shared_inputs}/create_temp_instance.py \ +# RUN: %s %{shared_inputs}/SmallInstance \ +# RUN: %t.instance %S/../../ui/Inputs/V4Pages_extra_records.sql +# +# RUN: python %s %t.instance +# END. + +import json +import logging +import sys +import unittest +import lnt.server.ui.app + +logging.basicConfig(level=logging.INFO) + + +class SchemaDeleteApiTest(unittest.TestCase): + """Test DELETE /schema endpoint for schema removal.""" + + def setUp(self): + _, instance_path = sys.argv + app = lnt.server.ui.app.App.create_standalone(instance_path) + app.testing = True + self.client = app.test_client() + + def _schema_payload(self, name): + return f""" +format_version: '2' +name: {name} +metrics: +- name: execution_time + type: Real + unit: seconds +run_fields: +- name: build_revision + order: true +machine_fields: +- name: hardware +""" + + def _post_schema(self, name): + payload = self._schema_payload(name) + return self.client.post( + f"api/db_default/v4/{name}/schema", + data=payload, + content_type="application/x-yaml", + headers={"AuthToken": "test_token"}, + ) + + def test_delete_requires_auth(self): + resp = self._post_schema("schema_delete_suite") + self.assertEqual(resp.status_code, 201, resp.data.decode("utf-8")) + + resp = self.client.delete( + "api/db_default/v4/schema_delete_suite/schema", + ) + self.assertEqual(resp.status_code, 401) + + def test_delete_schema_success(self): + resp = self._post_schema("schema_delete_suite") + self.assertEqual(resp.status_code, 201, resp.data.decode("utf-8")) + + resp = self.client.delete( + "api/db_default/v4/schema_delete_suite/schema", + headers={"AuthToken": "test_token"}, + ) + self.assertEqual(resp.status_code, 200, resp.data.decode("utf-8")) + result = json.loads(resp.data) + self.assertEqual(result["testsuite"], "schema_delete_suite") + + resp = self.client.get("api/db_default/v4/schema_delete_suite/schema") + self.assertEqual(resp.status_code, 404) + + def test_delete_unknown_suite(self): + resp = self.client.delete( + "api/db_default/v4/does_not_exist/schema", + headers={"AuthToken": "test_token"}, + ) + self.assertEqual(resp.status_code, 404) + + def test_delete_default_nts_suite(self): + resp = self.client.delete( + "api/db_default/v4/nts/schema", + headers={"AuthToken": "test_token"}, + ) + self.assertEqual(resp.status_code, 200, resp.data.decode("utf-8")) + result = json.loads(resp.data) + self.assertEqual(result["testsuite"], "nts") + + resp = self.client.get("api/db_default/v4/nts/schema") + self.assertEqual(resp.status_code, 404) + + +if __name__ == "__main__": + unittest.main(argv=[sys.argv[0], ])