From 8127f33b00eb635d883eb96ef524390fd66f594a Mon Sep 17 00:00:00 2001 From: Anthony Mahanna <43019056+aMahanna@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:39:26 -0500 Subject: [PATCH 1/6] new: `get/put /_admin/log/structured` (#307) * new: structured log settings * fix lint --- arango/database.py | 54 ++++++++++++++++++++++++++++++++++++++++-- arango/exceptions.py | 8 +++++++ tests/test_database.py | 16 +++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/arango/database.py b/arango/database.py index e33bd5ae..6de6c913 100644 --- a/arango/database.py +++ b/arango/database.py @@ -8,7 +8,7 @@ from datetime import datetime from numbers import Number -from typing import Any, List, Optional, Sequence, Union +from typing import Any, Dict, List, Optional, Sequence, Union from warnings import warn from arango.api import ApiGroup @@ -48,6 +48,8 @@ ServerLicenseSetError, ServerLogLevelError, ServerLogLevelSetError, + ServerLogSettingError, + ServerLogSettingSetError, ServerMetricsError, ServerReadLogError, ServerReloadRoutingError, @@ -749,6 +751,52 @@ def response_handler(resp: Response) -> Json: return self._execute(request, response_handler) + def log_settings(self) -> Result[Json]: + """Return the structured log settings. + + :return: Current log settings. False values are not returned. + :rtype: dict + """ + request = Request(method="get", endpoint="/_admin/log/structured") + + def response_handler(resp: Response) -> Json: + if not resp.is_success: + raise ServerLogSettingError(resp, request) + result: Json = resp.body + return result + + return self._execute(request, response_handler) + + def set_log_settings(self, **kwargs: Dict[str, Any]) -> Result[Json]: + """Set the structured log settings. + + This method takes arbitrary keyword arguments where the keys are the + structured log parameters and the values are true or false, for either + enabling or disabling the parameters. + + .. code-block:: python + + arango.set_log_settings( + database=True, + url=True, + username=False, + ) + + :param kwargs: Structured log parameters. + :type kwargs: Dict[str, Any] + :return: New log settings. False values are not returned. + :rtype: dict + """ + request = Request(method="put", endpoint="/_admin/log/structured", data=kwargs) + + def response_handler(resp: Response) -> Json: + if not resp.is_success: + raise ServerLogSettingSetError(resp, request) + result: Json = resp.body + return result + + return self._execute(request, response_handler) + def log_levels(self, server_id: Optional[str] = None) -> Result[Json]: """Return current logging levels. @@ -775,7 +823,7 @@ def response_handler(resp: Response) -> Json: return self._execute(request, response_handler) def set_log_levels( - self, server_id: Optional[str] = None, **kwargs: str + self, server_id: Optional[str] = None, **kwargs: Dict[str, Any] ) -> Result[Json]: """Set the logging levels. @@ -797,6 +845,8 @@ def set_log_levels( JWT authentication whereas Coordinators also support authentication using usernames and passwords. :type server_id: str | None + :param kwargs: Logging levels. + :type kwargs: Dict[str, Any] :return: New logging levels. :rtype: dict """ diff --git a/arango/exceptions.py b/arango/exceptions.py index fb11f8d5..525d112f 100644 --- a/arango/exceptions.py +++ b/arango/exceptions.py @@ -666,10 +666,18 @@ class ServerLogLevelError(ArangoServerError): """Failed to retrieve server log levels.""" +class ServerLogSettingError(ArangoServerError): + """Failed to retrieve server log settings.""" + + class ServerLogLevelSetError(ArangoServerError): """Failed to set server log levels.""" +class ServerLogSettingSetError(ArangoServerError): + """Failed to set server log settings.""" + + class ServerReloadRoutingError(ArangoServerError): """Failed to reload routing details.""" diff --git a/tests/test_database.py b/tests/test_database.py index da6307a4..13d8ac3e 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -253,6 +253,22 @@ def test_database_misc_methods(sys_db, db, bad_db, cluster): with assert_raises(ServerLogLevelSetError): bad_db.set_log_levels(**new_levels) + # Test Log Settings + result_1 = sys_db.set_log_settings(database=True, url=True, username=True) + result_2 = sys_db.log_settings() + assert isinstance(result_1, dict) + assert "database" in result_1 + assert "url" in result_1 + assert "username" in result_1 + assert result_1 == result_2 + + result_1 = sys_db.set_log_settings(database=True, username=False) + result_2 = sys_db.log_settings() + assert "database" in result_1 + assert "url" in result_1 + assert "username" not in result_1 + assert result_1 == result_2 + # Test get storage engine engine = db.engine() assert engine["name"] in ["rocksdb"] From cb338b6de29101966a18765d65a3b6ba5faf2e56 Mon Sep 17 00:00:00 2001 From: Anthony Mahanna <43019056+aMahanna@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:39:40 -0500 Subject: [PATCH 2/6] new: `put /_admin/compact` (#308) * new: `Database.compact()` * fix typo * remove useless `compact()` * assert empty `result` --- arango/database.py | 42 ++++++++++++++++++++++++++++++++++++++++++ arango/exceptions.py | 4 ++++ tests/test_database.py | 22 ++++++++++++++++++++-- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/arango/database.py b/arango/database.py index 6de6c913..408a4777 100644 --- a/arango/database.py +++ b/arango/database.py @@ -27,6 +27,7 @@ CollectionCreateError, CollectionDeleteError, CollectionListError, + DatabaseCompactError, DatabaseCreateError, DatabaseDeleteError, DatabaseListError, @@ -441,6 +442,47 @@ def response_handler(resp: Response) -> Json: return self._execute(request, response_handler) + def compact( + self, + change_level: Optional[bool] = None, + compact_bottom_most_level: Optional[bool] = None, + ) -> Result[Json]: + """Compact all databases. + + NOTE: This command can cause a full rewrite of all data in all databases, + which may take very long for large databases. It should thus only be used with + care and only when additional I/O load can be tolerated for a prolonged time. + + This method can be used to reclaim disk space after substantial data deletions + have taken place, by compacting the entire database system data. + + This method requires superuser access. + + :param change_level: Whether or not compacted data should be moved to + the minimum possible level. Default value is False. + :type change_level: bool | None + :param compact_bottom_most_level: Whether or not to compact the + bottom-most level of data. Default value is False. + :type compact_bottom_most_level: bool | None + :return: Collection compact. + :rtype: dict + :raise arango.exceptions.CollectionCompactError: If retrieval fails. + """ + data = {} + if change_level is not None: + data["changeLevel"] = change_level + if compact_bottom_most_level is not None: + data["compactBottomMostLevel"] = compact_bottom_most_level + + request = Request(method="put", endpoint="/_admin/compact", data=data) + + def response_handler(resp: Response) -> Json: + if resp.is_success: + return format_body(resp.body) + raise DatabaseCompactError(resp, request) + + return self._execute(request, response_handler) + def required_db_version(self) -> Result[str]: """Return required version of target database. diff --git a/arango/exceptions.py b/arango/exceptions.py index 525d112f..f3f4d264 100644 --- a/arango/exceptions.py +++ b/arango/exceptions.py @@ -360,6 +360,10 @@ class DatabaseDeleteError(ArangoServerError): """Failed to delete database.""" +class DatabaseCompactError(ArangoServerError): + """Failed to compact databases.""" + + ####################### # Document Exceptions # ####################### diff --git a/tests/test_database.py b/tests/test_database.py index 13d8ac3e..b7830700 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -13,6 +13,7 @@ USE_SYSTEM_DATABASE, ) from arango.exceptions import ( + DatabaseCompactError, DatabaseCreateError, DatabaseDeleteError, DatabaseListError, @@ -37,7 +38,12 @@ from arango.pregel import Pregel from arango.replication import Replication from arango.wal import WAL -from tests.helpers import assert_raises, generate_db_name +from tests.helpers import ( + assert_raises, + generate_col_name, + generate_db_name, + generate_jwt, +) def test_database_attributes(db, username): @@ -57,7 +63,7 @@ def test_database_attributes(db, username): assert isinstance(db.wal, WAL) -def test_database_misc_methods(sys_db, db, bad_db, cluster): +def test_database_misc_methods(client, sys_db, db, bad_db, cluster, secret): # Test get properties properties = db.properties() assert "id" in properties @@ -279,6 +285,18 @@ def test_database_misc_methods(sys_db, db, bad_db, cluster): bad_db.engine() assert err.value.error_code in {11, 1228} + # Test database compact + with assert_raises(DatabaseCompactError) as err: + db.compact() + + collection = db.create_collection(generate_col_name()) + collection.insert({"foo": "bar"}) + + token = generate_jwt(secret) + db_superuser = client.db(db.name, superuser_token=token) + result = db_superuser.compact() + assert result == {} + def test_database_management(db, sys_db, bad_db): # Test list databases From 6653c1bae075510f125997e84f098812c6a6c106 Mon Sep 17 00:00:00 2001 From: Anthony Mahanna <43019056+aMahanna@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:40:23 -0500 Subject: [PATCH 3/6] new: `post /_admin/echo` (#310) * new: `Database. echo_request()` * remove: `echo_request` --- arango/database.py | 17 +++++++++++++---- docs/admin.rst | 3 +++ tests/test_database.py | 6 ++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/arango/database.py b/arango/database.py index 408a4777..9faafa66 100644 --- a/arango/database.py +++ b/arango/database.py @@ -570,14 +570,23 @@ def response_handler(resp: Response) -> datetime: return self._execute(request, response_handler) - def echo(self) -> Result[Json]: - """Return details of the last request (e.g. headers, payload). - + def echo(self, body: Optional[Any] = None) -> Result[Json]: + """Return details of the last request (e.g. headers, payload), + or echo the given request body. + + :param body: The body of the request. Can be of any type + and is simply forwarded. If not set, the details of the last + request are returned. + :type body: dict | list | str | int | float | None :return: Details of the last request. :rtype: dict :raise arango.exceptions.ServerEchoError: If retrieval fails. """ - request = Request(method="get", endpoint="/_admin/echo") + request = ( + Request(method="get", endpoint="/_admin/echo") + if body is None + else Request(method="post", endpoint="/_admin/echo", data=body) + ) def response_handler(resp: Response) -> Json: if not resp.is_success: diff --git a/docs/admin.rst b/docs/admin.rst index 744b44b3..dc3dc030 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -54,6 +54,9 @@ database. # Echo the last request. sys_db.echo() + # Echo a request + sys_db.echo('request goes here') + # Reload the routing collection. sys_db.reload_routing() diff --git a/tests/test_database.py b/tests/test_database.py index b7830700..12d14a7d 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -172,6 +172,12 @@ def test_database_misc_methods(client, sys_db, db, bad_db, cluster, secret): bad_db.echo() assert err.value.error_code in {11, 1228} + # Test echo (forward request) + body = "request goes here" + echo = db.echo(body) + assert isinstance(echo, dict) + assert echo["requestBody"] == body + # Test read_log with default parameters # Deprecated in 3.8.0 # TODO: Remove in future release From 70548d92102200003fefc9a36810aa2ac5852b99 Mon Sep 17 00:00:00 2001 From: Anthony Mahanna <43019056+aMahanna@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:40:36 -0500 Subject: [PATCH 4/6] new: `get/put /_admin/server/mode` (#312) * new: `mode()`, `server_mode()`, `set_mode()` * fix: test getting read-only errors even after running `sys_db.set_mode("default")`. Probably sync issues.. --- arango/cluster.py | 22 ++++++++++++++++++ arango/database.py | 52 ++++++++++++++++++++++++++++++++++++++++++ arango/exceptions.py | 16 +++++++++++-- docs/admin.rst | 15 +++++++++++- tests/test_cluster.py | 13 +++++++++++ tests/test_database.py | 14 ++++++++++++ 6 files changed, 129 insertions(+), 3 deletions(-) diff --git a/arango/cluster.py b/arango/cluster.py index a272f50c..6ebdb69b 100644 --- a/arango/cluster.py +++ b/arango/cluster.py @@ -11,6 +11,7 @@ ClusterServerCountError, ClusterServerEngineError, ClusterServerIDError, + ClusterServerModeError, ClusterServerRoleError, ClusterServerStatisticsError, ClusterServerVersionError, @@ -57,6 +58,27 @@ def response_handler(resp: Response) -> str: return self._execute(request, response_handler) + def server_mode(self) -> Result[str]: + """Return the server mode. + + In a read-only server, all write operations will fail + with an error code of 1004 (ERROR_READ_ONLY). Creating or dropping + databases and collections will also fail with error code 11 (ERROR_FORBIDDEN). + + :return: Server mode. Possible values are "default" or "readonly". + :rtype: str + :raise arango.exceptions.ClusterServerModeError: If retrieval fails. + """ + request = Request(method="get", endpoint="/_admin/server/mode") + + def response_handler(resp: Response) -> str: + if resp.is_success: + return str(resp.body["mode"]) + + raise ClusterServerModeError(resp, request) + + return self._execute(request, response_handler) + def server_version(self, server_id: str) -> Result[Json]: """Return the version of the given server. diff --git a/arango/database.py b/arango/database.py index 9faafa66..aedeaba6 100644 --- a/arango/database.py +++ b/arango/database.py @@ -52,6 +52,8 @@ ServerLogSettingError, ServerLogSettingSetError, ServerMetricsError, + ServerModeError, + ServerModeSetError, ServerReadLogError, ServerReloadRoutingError, ServerRequiredDBVersionError, @@ -554,6 +556,56 @@ def response_handler(resp: Response) -> str: return self._execute(request, response_handler) + def mode(self) -> Result[str]: + """Return the server mode (default or read-only) + + In a read-only server, all write operations will fail + with an error code of 1004 (ERROR_READ_ONLY). Creating or dropping + databases and collections will also fail with error code 11 (ERROR_FORBIDDEN). + + :return: Server mode. Possible values are "default" or "readonly". + :rtype: str + :raise arango.exceptions.ServerModeError: If retrieval fails. + """ + request = Request(method="get", endpoint="/_admin/server/mode") + + def response_handler(resp: Response) -> str: + if resp.is_success: + return str(resp.body["mode"]) + + raise ServerModeError(resp, request) + + return self._execute(request, response_handler) + + def set_mode(self, mode: str) -> Result[Json]: + """Set the server mode to read-only or default. + + Update mode information about a server. The JSON response will + contain a field mode with the value readonly or default. + In a read-only server all write operations will fail with an error + code of 1004 (ERROR_READ_ONLY). Creating or dropping of databases + and collections will also fail with error code 11 (ERROR_FORBIDDEN). + + This is a protected API. It requires authentication and administrative + server rights. + + :param mode: Server mode. Possible values are "default" or "readonly". + :type mode: str + :return: Server mode. + :rtype: str + :raise arango.exceptions.ServerModeSetError: If set fails. + """ + request = Request( + method="put", endpoint="/_admin/server/mode", data={"mode": mode} + ) + + def response_handler(resp: Response) -> Json: + if resp.is_success: + return format_body(resp.body) + raise ServerModeSetError(resp, request) + + return self._execute(request, response_handler) + def time(self) -> Result[datetime]: """Return server system time. diff --git a/arango/exceptions.py b/arango/exceptions.py index f3f4d264..0c0aca26 100644 --- a/arango/exceptions.py +++ b/arango/exceptions.py @@ -695,7 +695,15 @@ class ServerMetricsError(ArangoServerError): class ServerRoleError(ArangoServerError): - """Failed to retrieve server role in a cluster.""" + """Failed to retrieve server role.""" + + +class ServerModeError(ArangoServerError): + """Failed to retrieve server mode.""" + + +class ServerModeSetError(ArangoServerError): + """Failed to set server mode.""" class ServerTLSError(ArangoServerError): @@ -980,7 +988,11 @@ class ClusterServerIDError(ArangoServerError): class ClusterServerRoleError(ArangoServerError): - """Failed to retrieve server role.""" + """Failed to retrieve server role in a cluster.""" + + +class ClusterServerModeError(ArangoServerError): + """Failed to retrieve server mode in a cluster.""" class ClusterServerStatisticsError(ArangoServerError): diff --git a/docs/admin.rst b/docs/admin.rst index dc3dc030..e1c1efeb 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -32,9 +32,22 @@ database. # Retrieve the server time. sys_db.time() - # Retrieve the server role in a cluster. + # Retrieve the server role. sys_db.role() + # Retrieve the server role in a cluster. + sys_db.cluster.server_role() + + # Retrieve the server mode. + sys_db.mode() + + # Retrieve the server mode in a cluster. + sys_db.cluster.server_mode() + + # Set the server mode. + sys_db.set_mode('readonly') + sys_db.set_mode('default') + # Retrieve the server statistics. sys_db.statistics() diff --git a/tests/test_cluster.py b/tests/test_cluster.py index bbc31778..3eda0bb0 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -12,6 +12,7 @@ ClusterServerCountError, ClusterServerEngineError, ClusterServerIDError, + ClusterServerModeError, ClusterServerRoleError, ClusterServerStatisticsError, ClusterServerVersionError, @@ -43,6 +44,18 @@ def test_cluster_server_role(sys_db, bad_db, cluster): assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} +def test_cluster_server_mode(sys_db, bad_db, cluster): + if not cluster: + pytest.skip("Only tested in a cluster setup") + + result = sys_db.cluster.server_mode() + assert result == "default" + + with assert_raises(ClusterServerModeError) as err: + bad_db.cluster.server_mode() + assert err.value.error_code in {FORBIDDEN, DATABASE_NOT_FOUND} + + def test_cluster_health(sys_db, bad_db, cluster): if not cluster: pytest.skip("Only tested in a cluster setup") diff --git a/tests/test_database.py b/tests/test_database.py index 12d14a7d..8645b909 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -25,6 +25,7 @@ ServerLogLevelError, ServerLogLevelSetError, ServerMetricsError, + ServerModeSetError, ServerReadLogError, ServerReloadRoutingError, ServerRequiredDBVersionError, @@ -138,6 +139,19 @@ def test_database_misc_methods(client, sys_db, db, bad_db, cluster, secret): bad_db.role() assert err.value.error_code in {11, 1228} + # Test get/set server mode + assert sys_db.mode() == "default" + with assert_raises(ServerModeSetError): + sys_db.set_mode("badmode") + assert err.value.error_code in {11, 1228} + + with assert_raises(ServerModeSetError): + db.set_mode("readonly") + assert err.value.error_code in {11, 1228} + + result = sys_db.set_mode("default") + assert result == {"mode": "default"} + # Test get server status status = db.status() assert "host" in status From 126588196b9c93e2f4d787afa106aaf68141f404 Mon Sep 17 00:00:00 2001 From: Anthony Mahanna <43019056+aMahanna@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:54:40 -0500 Subject: [PATCH 5/6] new: `post /_admin/execute` (#309) * new: `Database.execute()` * fix lint * fix end of file * fix: lint --- arango/database.py | 31 +++++++++++++++++++++++++++++++ arango/exceptions.py | 4 ++++ arango/replication.py | 16 +++++++++------- tests/static/cluster-3.10.conf | 1 + tests/static/cluster.conf | 1 + tests/static/single-3.10.conf | 1 + tests/static/single.conf | 1 + tests/test_database.py | 6 ++++++ 8 files changed, 54 insertions(+), 7 deletions(-) diff --git a/arango/database.py b/arango/database.py index aedeaba6..734fd7ca 100644 --- a/arango/database.py +++ b/arango/database.py @@ -45,6 +45,7 @@ ServerEchoError, ServerEncryptionError, ServerEngineError, + ServerExecuteError, ServerLicenseGetError, ServerLicenseSetError, ServerLogLevelError, @@ -229,6 +230,36 @@ def response_handler(resp: Response) -> Json: return self._execute(request, response_handler) + def execute(self, command: str) -> Result[Any]: + """Execute raw Javascript command on the server. + + Executes the JavaScript code in the body on the server as + the body of a function with no arguments. If you have a + return statement then the return value you produce will be returned + as 'application/json'. + + NOTE: this method endpoint will only be usable if the server + was started with the option `--javascript.allow-admin-execute true`. + The default value of this option is false, which disables the execution + of user-defined code and disables this API endpoint entirely. + This is also the recommended setting for production. + + :param command: Javascript command to execute. + :type command: str + :return: Return value of **command**, if any. + :rtype: Any + :raise arango.exceptions.ServerExecuteError: If execution fails. + """ + request = Request(method="post", endpoint="/_admin/execute", data=command) + + def response_handler(resp: Response) -> Any: + if not resp.is_success: + raise ServerExecuteError(resp, request) + + return resp.body + + return self._execute(request, response_handler) + def execute_transaction( self, command: str, diff --git a/arango/exceptions.py b/arango/exceptions.py index 0c0aca26..8e7b807d 100644 --- a/arango/exceptions.py +++ b/arango/exceptions.py @@ -718,6 +718,10 @@ class ServerEncryptionError(ArangoServerError): """Failed to reload user-defined encryption keys.""" +class ServerExecuteError(ArangoServerError): + """Failed to execute raw JavaScript command.""" + + ##################### # Task Exceptions # ##################### diff --git a/arango/replication.py b/arango/replication.py index 0ecfc20e..d5fae457 100644 --- a/arango/replication.py +++ b/arango/replication.py @@ -180,13 +180,15 @@ def response_handler(resp: Response) -> Json: if resp.is_success: result = format_replication_header(resp.headers) result["content"] = [ - [ - self._conn.deserialize(line) - for line in resp.body.split("\n") - if line - ] - if deserialize - else resp.body + ( + [ + self._conn.deserialize(line) + for line in resp.body.split("\n") + if line + ] + if deserialize + else resp.body + ) ] return result diff --git a/tests/static/cluster-3.10.conf b/tests/static/cluster-3.10.conf index 573c030a..d7732c90 100644 --- a/tests/static/cluster-3.10.conf +++ b/tests/static/cluster-3.10.conf @@ -10,3 +10,4 @@ jwt-secret = /tests/static/keyfile [args] all.database.password = passwd all.log.api-enabled = true +all.javascript.allow-admin-execute = true diff --git a/tests/static/cluster.conf b/tests/static/cluster.conf index 182f3d17..86f78556 100644 --- a/tests/static/cluster.conf +++ b/tests/static/cluster.conf @@ -11,3 +11,4 @@ jwt-secret = /tests/static/keyfile all.database.password = passwd all.database.extended-names = true all.log.api-enabled = true +all.javascript.allow-admin-execute = true diff --git a/tests/static/single-3.10.conf b/tests/static/single-3.10.conf index c982303b..09d1d9f3 100644 --- a/tests/static/single-3.10.conf +++ b/tests/static/single-3.10.conf @@ -8,3 +8,4 @@ jwt-secret = /tests/static/keyfile [args] all.database.password = passwd +all.javascript.allow-admin-execute = true diff --git a/tests/static/single.conf b/tests/static/single.conf index e880f9d5..df45cb76 100644 --- a/tests/static/single.conf +++ b/tests/static/single.conf @@ -9,3 +9,4 @@ jwt-secret = /tests/static/keyfile [args] all.database.password = passwd all.database.extended-names = true +all.javascript.allow-admin-execute = true diff --git a/tests/test_database.py b/tests/test_database.py index 8645b909..cb43cbf2 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -305,6 +305,12 @@ def test_database_misc_methods(client, sys_db, db, bad_db, cluster, secret): bad_db.engine() assert err.value.error_code in {11, 1228} + # Test execute JavaScript code + assert db.execute(1) is None + assert db.execute(None) == {"error": False, "code": 200} + assert db.execute("") == {"error": False, "code": 200} + assert db.execute("return 1") == 1 + # Test database compact with assert_raises(DatabaseCompactError) as err: db.compact() From 805425de1cc303a427a0050ec148be7a37d4518c Mon Sep 17 00:00:00 2001 From: Anthony Mahanna <43019056+aMahanna@users.noreply.github.com> Date: Fri, 26 Jan 2024 13:02:42 -0500 Subject: [PATCH 6/6] new: `get /_admin/support-info` (#311) * new: `Database.support_info()` * remove `host` assertion * fix lint --- arango/database.py | 33 +++++++++++++++++++++++++++++++++ arango/exceptions.py | 4 ++++ tests/test_database.py | 9 +++++++++ 3 files changed, 46 insertions(+) diff --git a/arango/database.py b/arango/database.py index 734fd7ca..32defeab 100644 --- a/arango/database.py +++ b/arango/database.py @@ -32,6 +32,7 @@ DatabaseDeleteError, DatabaseListError, DatabasePropertiesError, + DatabaseSupportInfoError, GraphCreateError, GraphDeleteError, GraphListError, @@ -2859,6 +2860,38 @@ def response_handler(resp: Response) -> bool: return self._execute(request, response_handler) + ########### + # Support # + ########### + + def support_info(self) -> Result[Json]: + """Return information about the deployment. + + Retrieves deployment information for support purposes. + The endpoint returns data about the ArangoDB version used, + the host (operating system, server ID, CPU and storage capacity, + current utilization, a few metrics) and the other servers in the + deployment (in case of Active Failover or cluster deployments). + + NOTE: This method can only be accessed from inside the **_system** database. + The is a policy control startup option `--server.support-info-api` that controls + if and to whom the API is made available. + + :return: Deployment information. + :rtype: dict + :raise arango.exceptions.DatabaseSupportInfoError: If retrieval fails. + """ + request = Request(method="get", endpoint="/_admin/support-info") + + def response_handler(resp: Response) -> Json: + if resp.is_success: + result: Json = resp.body + return result + + raise DatabaseSupportInfoError(resp, request) + + return self._execute(request, response_handler) + class StandardDatabase(Database): """Standard database API wrapper.""" diff --git a/arango/exceptions.py b/arango/exceptions.py index 8e7b807d..000a0f8f 100644 --- a/arango/exceptions.py +++ b/arango/exceptions.py @@ -360,6 +360,10 @@ class DatabaseDeleteError(ArangoServerError): """Failed to delete database.""" +class DatabaseSupportInfoError(ArangoServerError): + """Failed to retrieve support info for deployment.""" + + class DatabaseCompactError(ArangoServerError): """Failed to compact databases.""" diff --git a/tests/test_database.py b/tests/test_database.py index cb43cbf2..23f2c7f4 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -18,6 +18,7 @@ DatabaseDeleteError, DatabaseListError, DatabasePropertiesError, + DatabaseSupportInfoError, ServerDetailsError, ServerEchoError, ServerEngineError, @@ -305,6 +306,14 @@ def test_database_misc_methods(client, sys_db, db, bad_db, cluster, secret): bad_db.engine() assert err.value.error_code in {11, 1228} + with assert_raises(DatabaseSupportInfoError) as err: + db.support_info() + + info = sys_db.support_info() + assert isinstance(info, dict) + assert "deployment" in info + assert "date" in info + # Test execute JavaScript code assert db.execute(1) is None assert db.execute(None) == {"error": False, "code": 200}