From ed533a106d10fb18a5de7b9a9501ce54fa4f22c4 Mon Sep 17 00:00:00 2001 From: alejandro Date: Wed, 1 Feb 2023 14:32:54 +0100 Subject: [PATCH 01/38] feat: Add helper function for creating a brancher in hypernode-api --- hypernode_api_python/client.py | 14 +++++++++++ tests/client/test_create_brancher.py | 36 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/client/test_create_brancher.py diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 52b5b03..c621404 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -9,6 +9,7 @@ HYPERNODE_API_APP_DETAIL_WITH_ADDONS_ENDPOINT = "/v2/app/{}/with_addons?destroyed=false" HYPERNODE_API_APP_EAV_DESCRIPTION_ENDPOINT = "/v2/app/eav_descriptions/" HYPERNODE_API_APP_FLAVOR_ENDPOINT = "/v2/app/{}/flavor/" +HYPERNODE_API_APP_BRANCHER_ENDPOINT = "/v2/app/{}/brancher/" HYPERNODE_API_APP_NEXT_BEST_PLAN_ENDPOINT = "/v2/app/{}/next_best_plan/" HYPERNODE_API_APP_PRODUCT_LIST_ENDPOINT = "/v2/product/app/{}/" HYPERNODE_API_APP_XGRADE_CHECK_ENDPOINT = "/v2/app/xgrade/{}/check/{}/" @@ -628,3 +629,16 @@ def order_hypernode(self, data): :return obj response: The request response object """ return self.requests("POST", HYPERNODE_API_APP_ORDER_ENDPOINT, data=data) + + def create_brancher(self, app_name, data): + """ + Create a new branch (server replica) of your Hypernode. + + :param str app_name: The name of the Hypernode to create the branch from + :param dict data: Data regarding the branch to be created. An example could be: + {'clear': 'mysql'}. + :return obj response: The request response object + """ + return self.requests( + "POST", HYPERNODE_API_APP_BRANCHER_ENDPOINT.format(app_name), data=data + ) diff --git a/tests/client/test_create_brancher.py b/tests/client/test_create_brancher.py new file mode 100644 index 0000000..33d3940 --- /dev/null +++ b/tests/client/test_create_brancher.py @@ -0,0 +1,36 @@ +from unittest import TestCase +from unittest.mock import Mock + +from hypernode_api_python.client import ( + HYPERNODE_API_APP_BRANCHER_ENDPOINT, + HypernodeAPIPython, +) + + +class TestCreateBrancher(TestCase): + def setUp(self): + self.client = HypernodeAPIPython(token="my_token") + self.mock_request = Mock() + self.client.requests = self.mock_request + self.app_name = "my_app" + + def test_brancher_endpoint_is_correct(self): + self.assertEqual("/v2/app/{}/brancher/", HYPERNODE_API_APP_BRANCHER_ENDPOINT) + + def test_calls_create_brancher_endpoint_properly(self): + data = {"clear_services": ["mysql"]} + self.client.create_brancher(self.app_name, data) + + self.mock_request.assert_called_once_with( + "POST", f"/v2/app/{self.app_name}/brancher/", data=data + ) + + def test_returns_create_brancher_data(self): + response = Mock() + response.status_code = 200 + self.mock_request.return_value = response + + self.assertEqual( + self.client.create_brancher(self.app_name, {}), + self.mock_request.return_value, + ) From 4eef4f9e83122aaa19987554ddc7084e4923a187 Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Fri, 3 Feb 2023 09:52:35 +0100 Subject: [PATCH 02/38] HNWEB-5220-add-active-branchers-endpoint --- hypernode_api_python/client.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index c621404..6742368 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -630,6 +630,19 @@ def order_hypernode(self, data): """ return self.requests("POST", HYPERNODE_API_APP_ORDER_ENDPOINT, data=data) + def get_active_branchers(self, app_name): + """ + List all active brancher nodes of your Hypernode. + + :param str app_name: The name of the Hypernode to create the branch from + :return obj response: The request response object + """ + HYPERNODE_API_APP_BRANCHER_ENDPOINT = "/v2/app/{}/brancher/" + + return self.requests( + "GET", HYPERNODE_API_APP_BRANCHER_ENDPOINT.format(app_name) + ) + def create_brancher(self, app_name, data): """ Create a new branch (server replica) of your Hypernode. From 8e0f908b4f458a6e8a2cf12023115e07d0b01ed6 Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Fri, 3 Feb 2023 12:38:18 +0100 Subject: [PATCH 03/38] add tests for get_active_branchers endpoint --- tests/client/test_get_active_branchers.py | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/client/test_get_active_branchers.py diff --git a/tests/client/test_get_active_branchers.py b/tests/client/test_get_active_branchers.py new file mode 100644 index 0000000..4b76bc3 --- /dev/null +++ b/tests/client/test_get_active_branchers.py @@ -0,0 +1,35 @@ +from unittest import TestCase +from unittest.mock import Mock + +from hypernode_api_python.client import ( + HYPERNODE_API_APP_BRANCHER_ENDPOINT, + HypernodeAPIPython, +) + + +class TestGetActiveBranchers(TestCase): + def setUp(self): + self.client = HypernodeAPIPython(token="my_token") + self.mock_request = Mock() + self.client.requests = self.mock_request + self.app_name = "my_app" + + def test_brancher_endpoint_is_correct(self): + self.assertEqual("/v2/app/{}/brancher/", HYPERNODE_API_APP_BRANCHER_ENDPOINT) + + def test_calls_get_active_branchers_endpoint_properly(self): + self.client.get_active_branchers(self.app_name) + + self.mock_request.assert_called_once_with( + "GET", f"/v2/app/{self.app_name}/brancher/" + ) + + def test_returns_active_branchers_data(self): + response = Mock() + response.status_code = 200 + self.mock_request.return_value = response + + self.assertEqual( + self.client.get_active_branchers(self.app_name), + self.mock_request.return_value, + ) From c39e9fd65d7c19971e3080dc3b43af4112e86a63 Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Fri, 3 Feb 2023 12:46:22 +0100 Subject: [PATCH 04/38] improve description of endpoint --- hypernode_api_python/client.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 6742368..ba48341 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -633,8 +633,30 @@ def order_hypernode(self, data): def get_active_branchers(self, app_name): """ List all active brancher nodes of your Hypernode. + Example: + > client.get_active_branchers('yourhypernodeappname').json() + > { + > "totalTime": 9417.327453, + > "totalAmountSpent": 9417.327453, + > "branchers": [ + > { + > "id": 21, + > "name": "yourhypernodeappname-eph123456", + > "ip": "127.0.0.1", + > "elapsed_time": "5008.662653", + > "description": "PHP version 7.1" + > }, + > { + > "id": 22, + > "name": "yourhypernodeappname-eph654321", + > "ip": "52.68.96.58", + > "elapsed_time": "4408.6648", + > "description": null + > } + > ] + > } - :param str app_name: The name of the Hypernode to create the branch from + :param str app_name: The name of the Hypernode to get your active branchers for :return obj response: The request response object """ HYPERNODE_API_APP_BRANCHER_ENDPOINT = "/v2/app/{}/brancher/" From 20df9760aacb1a5400585d2af093c79fa003c588 Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Fri, 3 Feb 2023 13:12:01 +0100 Subject: [PATCH 05/38] fix totalTime type typo --- hypernode_api_python/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index ba48341..ed46464 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -636,7 +636,7 @@ def get_active_branchers(self, app_name): Example: > client.get_active_branchers('yourhypernodeappname').json() > { - > "totalTime": 9417.327453, + > "totalTime": "9417.327453", > "totalAmountSpent": 9417.327453, > "branchers": [ > { From 2af1104e35a81b3a500a57dc47762da70e9c8f30 Mon Sep 17 00:00:00 2001 From: alejandro Date: Mon, 6 Feb 2023 10:55:37 +0100 Subject: [PATCH 06/38] feat: update package to newer version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ac60f8d..f25a4b0 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( name="hypernode_api_python", - version="0.0.2", + version="0.0.3", description='"Hypernode API Client for Python"', url="https://github.com/ByteInternet/hypernode_api_python", packages=find_packages( From 9aa43226cc88b0ead65a3b943bc263f0866a65dd Mon Sep 17 00:00:00 2001 From: alejandro Date: Mon, 6 Feb 2023 10:56:03 +0100 Subject: [PATCH 07/38] fix: update docs for new function --- hypernode_api_python/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index c621404..0800fd9 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -636,7 +636,7 @@ def create_brancher(self, app_name, data): :param str app_name: The name of the Hypernode to create the branch from :param dict data: Data regarding the branch to be created. An example could be: - {'clear': 'mysql'}. + {'clear_services': ['mysql', 'cron']}. :return obj response: The request response object """ return self.requests( From 47fee329f47780a7d93e8aebcc6b61555a64c6da Mon Sep 17 00:00:00 2001 From: alejandro Date: Mon, 6 Feb 2023 11:00:55 +0100 Subject: [PATCH 08/38] feat: update version number in readme file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 72e6a6f..6725bc6 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ $ python3 -m venv venv $ . venv/bin/activate $ pip install hypernode-api-python $ pip freeze | grep hypernode-api-python -hypernode-api-python==0.0.2 +hypernode-api-python==0.0.3 ``` ### Performing API calls From 1e66f6cae23e47f05bf31443f0bf9c9ee6b8f915 Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Tue, 7 Feb 2023 11:17:44 +0100 Subject: [PATCH 09/38] update formats of get_active_brancher fields --- hypernode_api_python/client.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index ed46464..6faa89a 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -634,24 +634,24 @@ def get_active_branchers(self, app_name): """ List all active brancher nodes of your Hypernode. Example: - > client.get_active_branchers('yourhypernodeappname').json() + > client.get_active_branchers("yourhypernodeappname").json() > { - > "totalTime": "9417.327453", - > "totalAmountSpent": 9417.327453, + > "total_time": 15048, + > "total_amount_spent": 252, > "branchers": [ > { > "id": 21, > "name": "yourhypernodeappname-eph123456", > "ip": "127.0.0.1", - > "elapsed_time": "5008.662653", + > "elapsed_time": 7824, > "description": "PHP version 7.1" > }, > { > "id": 22, > "name": "yourhypernodeappname-eph654321", > "ip": "52.68.96.58", - > "elapsed_time": "4408.6648", - > "description": null + > "elapsed_time": 7224, + > "description": None > } > ] > } @@ -659,8 +659,6 @@ def get_active_branchers(self, app_name): :param str app_name: The name of the Hypernode to get your active branchers for :return obj response: The request response object """ - HYPERNODE_API_APP_BRANCHER_ENDPOINT = "/v2/app/{}/brancher/" - return self.requests( "GET", HYPERNODE_API_APP_BRANCHER_ENDPOINT.format(app_name) ) From 02cd0558f0fcc5b2fd678c13a049e69cdc17af6e Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Tue, 7 Feb 2023 12:25:37 +0100 Subject: [PATCH 10/38] remove total time and price fields for now --- hypernode_api_python/client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 6faa89a..f2cee6d 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -636,8 +636,6 @@ def get_active_branchers(self, app_name): Example: > client.get_active_branchers("yourhypernodeappname").json() > { - > "total_time": 15048, - > "total_amount_spent": 252, > "branchers": [ > { > "id": 21, From 811ef8d07c93ebd0fb64cab05dd174fa3d7c7075 Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Tue, 7 Feb 2023 15:57:54 +0100 Subject: [PATCH 11/38] add monthly total cost and time in example --- hypernode_api_python/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index f2cee6d..8d351c1 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -636,6 +636,8 @@ def get_active_branchers(self, app_name): Example: > client.get_active_branchers("yourhypernodeappname").json() > { + > "monthly_total_time": 6397, + > "monthly_total_cost": 109, > "branchers": [ > { > "id": 21, From 80ad9cf37279c3f2db49e5e10f65ef94a5520c93 Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Wed, 8 Feb 2023 09:38:33 +0100 Subject: [PATCH 12/38] add created field --- hypernode_api_python/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 8d351c1..21aeecf 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -642,6 +642,7 @@ def get_active_branchers(self, app_name): > { > "id": 21, > "name": "yourhypernodeappname-eph123456", + > "created": "2019-08-24T14:15:22Z" > "ip": "127.0.0.1", > "elapsed_time": 7824, > "description": "PHP version 7.1" @@ -650,6 +651,7 @@ def get_active_branchers(self, app_name): > "id": 22, > "name": "yourhypernodeappname-eph654321", > "ip": "52.68.96.58", + > "created": "2019-08-24T14:15:22Z" > "elapsed_time": 7224, > "description": None > } From 2cf1eb112173f985368603475c3553ff70b18399 Mon Sep 17 00:00:00 2001 From: Mark den Toom Date: Thu, 9 Feb 2023 15:53:14 +0100 Subject: [PATCH 13/38] Remove description field from get_active_branchers --- hypernode_api_python/client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 21aeecf..9f6d638 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -645,7 +645,6 @@ def get_active_branchers(self, app_name): > "created": "2019-08-24T14:15:22Z" > "ip": "127.0.0.1", > "elapsed_time": 7824, - > "description": "PHP version 7.1" > }, > { > "id": 22, @@ -653,7 +652,6 @@ def get_active_branchers(self, app_name): > "ip": "52.68.96.58", > "created": "2019-08-24T14:15:22Z" > "elapsed_time": 7224, - > "description": None > } > ] > } From 8a30fd7cbb50d0a0541359033e27cab5123d0c0b Mon Sep 17 00:00:00 2001 From: alejandro Date: Fri, 10 Feb 2023 11:26:41 +0100 Subject: [PATCH 14/38] feat: update version number in package and readme --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6725bc6..dae8cfd 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ $ python3 -m venv venv $ . venv/bin/activate $ pip install hypernode-api-python $ pip freeze | grep hypernode-api-python -hypernode-api-python==0.0.3 +hypernode-api-python==0.0.4 ``` ### Performing API calls diff --git a/setup.py b/setup.py index f25a4b0..58eb588 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( name="hypernode_api_python", - version="0.0.3", + version="0.0.4", description='"Hypernode API Client for Python"', url="https://github.com/ByteInternet/hypernode_api_python", packages=find_packages( From c7a6be5fae78c79f392391c618c3d018f3e27dcd Mon Sep 17 00:00:00 2001 From: alejandro Date: Fri, 10 Feb 2023 15:18:03 +0100 Subject: [PATCH 15/38] feat: add destroy brancher endpoint --- hypernode_api_python/client.py | 12 +++++++++ tests/client/test_destroy_brancher.py | 35 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/client/test_destroy_brancher.py diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 51ece44..31a733d 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -10,6 +10,7 @@ HYPERNODE_API_APP_EAV_DESCRIPTION_ENDPOINT = "/v2/app/eav_descriptions/" HYPERNODE_API_APP_FLAVOR_ENDPOINT = "/v2/app/{}/flavor/" HYPERNODE_API_APP_BRANCHER_ENDPOINT = "/v2/app/{}/brancher/" +HYPERNODE_API_BRANCHER_ENDPOINT = "/v2/brancher/{}/" HYPERNODE_API_APP_NEXT_BEST_PLAN_ENDPOINT = "/v2/app/{}/next_best_plan/" HYPERNODE_API_APP_PRODUCT_LIST_ENDPOINT = "/v2/product/app/{}/" HYPERNODE_API_APP_XGRADE_CHECK_ENDPOINT = "/v2/app/xgrade/{}/check/{}/" @@ -675,3 +676,14 @@ def create_brancher(self, app_name, data): return self.requests( "POST", HYPERNODE_API_APP_BRANCHER_ENDPOINT.format(app_name), data=data ) + + def destroy_brancher(self, brancher_name): + """ + Destroy an existing brancher node of your Hypernode. + + :param str brancher_name: The name of the brancher node to destroy. + :return obj response: The request response object + """ + return self.requests( + "DELETE", HYPERNODE_API_BRANCHER_ENDPOINT.format(brancher_name) + ) diff --git a/tests/client/test_destroy_brancher.py b/tests/client/test_destroy_brancher.py new file mode 100644 index 0000000..698ef4a --- /dev/null +++ b/tests/client/test_destroy_brancher.py @@ -0,0 +1,35 @@ +from unittest import TestCase +from unittest.mock import Mock + +from hypernode_api_python.client import ( + HypernodeAPIPython, + HYPERNODE_API_BRANCHER_ENDPOINT, +) + + +class TestDestroyBrancher(TestCase): + def setUp(self): + self.client = HypernodeAPIPython(token="my_token") + self.mock_request = Mock() + self.client.requests = self.mock_request + self.brancher_name = "app-branchermobyname" + + def test_brancher_endpoint_is_correct(self): + self.assertEqual("/v2/brancher/{}/", HYPERNODE_API_BRANCHER_ENDPOINT) + + def test_calls_destroy_brancher_endpoint_properly(self): + self.client.destroy_brancher(self.brancher_name) + + self.mock_request.assert_called_once_with( + "DELETE", HYPERNODE_API_BRANCHER_ENDPOINT.format(self.brancher_name) + ) + + def test_returns_destroy_brancher_data(self): + response = Mock() + response.status_code = 204 + self.mock_request.return_value = response + + self.assertEqual( + self.client.destroy_brancher(self.brancher_name), + self.mock_request.return_value, + ) From 152760275601b3b6ca731b3e3a56346f3f982ba8 Mon Sep 17 00:00:00 2001 From: alejandro Date: Fri, 10 Feb 2023 16:14:54 +0100 Subject: [PATCH 16/38] feat: update version to 0.0.5 --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dae8cfd..aa32798 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ $ python3 -m venv venv $ . venv/bin/activate $ pip install hypernode-api-python $ pip freeze | grep hypernode-api-python -hypernode-api-python==0.0.4 +hypernode-api-python==0.0.5 ``` ### Performing API calls diff --git a/setup.py b/setup.py index 58eb588..d319956 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( name="hypernode_api_python", - version="0.0.4", + version="0.0.5", description='"Hypernode API Client for Python"', url="https://github.com/ByteInternet/hypernode_api_python", packages=find_packages( From e0f4a6bf037a187456b8482d564dd072d9f34f5f Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Wed, 15 Feb 2023 11:33:32 +0100 Subject: [PATCH 17/38] add-labels-to-brancher-app-list --- hypernode_api_python/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 31a733d..321e790 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -646,6 +646,7 @@ def get_active_branchers(self, app_name): > "created": "2019-08-24T14:15:22Z" > "ip": "127.0.0.1", > "elapsed_time": 7824, + > "labels": {"description": "php upgrade test", "label without equal sign": None}, > }, > { > "id": 22, @@ -653,6 +654,7 @@ def get_active_branchers(self, app_name): > "ip": "52.68.96.58", > "created": "2019-08-24T14:15:22Z" > "elapsed_time": 7224, + > "labels": {}, > } > ] > } From 3e4c1ff73d9851e09d1169dba49efc5573879e8c Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Thu, 23 Feb 2023 11:13:37 +0100 Subject: [PATCH 18/38] add-cost-and-end_time-to-get_active_branchers --- hypernode_api_python/client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 321e790..39ce95c 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -643,16 +643,20 @@ def get_active_branchers(self, app_name): > { > "id": 21, > "name": "yourhypernodeappname-eph123456", + > "cost": 131, > "created": "2019-08-24T14:15:22Z" > "ip": "127.0.0.1", + > "end_time": None, > "elapsed_time": 7824, > "labels": {"description": "php upgrade test", "label without equal sign": None}, > }, > { > "id": 22, > "name": "yourhypernodeappname-eph654321", + > "cost": 121, > "ip": "52.68.96.58", > "created": "2019-08-24T14:15:22Z" + > "end_time": None, > "elapsed_time": 7224, > "labels": {}, > } From 0ebe0976f880a85dc8384198451fec8e65ecebf4 Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Thu, 23 Feb 2023 11:27:19 +0100 Subject: [PATCH 19/38] use-new-brancher-app-endpoint --- hypernode_api_python/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 321e790..3899848 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -9,7 +9,7 @@ HYPERNODE_API_APP_DETAIL_WITH_ADDONS_ENDPOINT = "/v2/app/{}/with_addons?destroyed=false" HYPERNODE_API_APP_EAV_DESCRIPTION_ENDPOINT = "/v2/app/eav_descriptions/" HYPERNODE_API_APP_FLAVOR_ENDPOINT = "/v2/app/{}/flavor/" -HYPERNODE_API_APP_BRANCHER_ENDPOINT = "/v2/app/{}/brancher/" +HYPERNODE_API_BRANCHER_APP_ENDPOINT = "/v2/brancher/app/{}/" HYPERNODE_API_BRANCHER_ENDPOINT = "/v2/brancher/{}/" HYPERNODE_API_APP_NEXT_BEST_PLAN_ENDPOINT = "/v2/app/{}/next_best_plan/" HYPERNODE_API_APP_PRODUCT_LIST_ENDPOINT = "/v2/product/app/{}/" @@ -663,7 +663,7 @@ def get_active_branchers(self, app_name): :return obj response: The request response object """ return self.requests( - "GET", HYPERNODE_API_APP_BRANCHER_ENDPOINT.format(app_name) + "GET", HYPERNODE_API_BRANCHER_APP_ENDPOINT.format(app_name) ) def create_brancher(self, app_name, data): @@ -676,7 +676,7 @@ def create_brancher(self, app_name, data): :return obj response: The request response object """ return self.requests( - "POST", HYPERNODE_API_APP_BRANCHER_ENDPOINT.format(app_name), data=data + "POST", HYPERNODE_API_BRANCHER_APP_ENDPOINT.format(app_name), data=data ) def destroy_brancher(self, brancher_name): From 96d78710daf3d648eff3bb10b0ab0ec2a7f51857 Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Thu, 23 Feb 2023 11:38:19 +0100 Subject: [PATCH 20/38] update tests --- tests/client/test_create_brancher.py | 6 +++--- tests/client/test_get_active_branchers.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/client/test_create_brancher.py b/tests/client/test_create_brancher.py index 33d3940..f980681 100644 --- a/tests/client/test_create_brancher.py +++ b/tests/client/test_create_brancher.py @@ -2,7 +2,7 @@ from unittest.mock import Mock from hypernode_api_python.client import ( - HYPERNODE_API_APP_BRANCHER_ENDPOINT, + HYPERNODE_API_BRANCHER_APP_ENDPOINT, HypernodeAPIPython, ) @@ -15,14 +15,14 @@ def setUp(self): self.app_name = "my_app" def test_brancher_endpoint_is_correct(self): - self.assertEqual("/v2/app/{}/brancher/", HYPERNODE_API_APP_BRANCHER_ENDPOINT) + self.assertEqual("/v2/brancher/app/{}/", HYPERNODE_API_BRANCHER_APP_ENDPOINT) def test_calls_create_brancher_endpoint_properly(self): data = {"clear_services": ["mysql"]} self.client.create_brancher(self.app_name, data) self.mock_request.assert_called_once_with( - "POST", f"/v2/app/{self.app_name}/brancher/", data=data + "POST", f"/v2/brancher/app/{self.app_name}/", data=data ) def test_returns_create_brancher_data(self): diff --git a/tests/client/test_get_active_branchers.py b/tests/client/test_get_active_branchers.py index 4b76bc3..bb71fc9 100644 --- a/tests/client/test_get_active_branchers.py +++ b/tests/client/test_get_active_branchers.py @@ -2,7 +2,7 @@ from unittest.mock import Mock from hypernode_api_python.client import ( - HYPERNODE_API_APP_BRANCHER_ENDPOINT, + HYPERNODE_API_BRANCHER_APP_ENDPOINT, HypernodeAPIPython, ) @@ -15,13 +15,13 @@ def setUp(self): self.app_name = "my_app" def test_brancher_endpoint_is_correct(self): - self.assertEqual("/v2/app/{}/brancher/", HYPERNODE_API_APP_BRANCHER_ENDPOINT) + self.assertEqual("/v2/brancher/app/{}/", HYPERNODE_API_BRANCHER_APP_ENDPOINT) def test_calls_get_active_branchers_endpoint_properly(self): self.client.get_active_branchers(self.app_name) self.mock_request.assert_called_once_with( - "GET", f"/v2/app/{self.app_name}/brancher/" + "GET", f"/v2/brancher/app/{self.app_name}/" ) def test_returns_active_branchers_data(self): From 8b27f2f78c1f8259422d42c30304df1fa07df8ac Mon Sep 17 00:00:00 2001 From: "mark.dentoom" Date: Thu, 23 Feb 2023 11:38:55 +0100 Subject: [PATCH 21/38] add-tox-to-gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0cf3930..1e20d72 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ __pycache__ # misc .idea +.tox \ No newline at end of file From 4b717bfdc67f13a4ccbd35a387fd68e4de0623dd Mon Sep 17 00:00:00 2001 From: Mark den Toom Date: Thu, 23 Feb 2023 12:18:44 +0100 Subject: [PATCH 22/38] Add EOF newline --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1e20d72..ffd4289 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,4 @@ __pycache__ # misc .idea -.tox \ No newline at end of file +.tox From 803d982ebd35e99aeb5392bbd1312eae55a67ddd Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Sun, 5 Mar 2023 15:45:05 +0100 Subject: [PATCH 23/38] add method for listing flows so the API client can be used to see the current running and historical flows for a specific app. this is the same information that hypernode-log would show on a hypernode. --- hypernode_api_python/client.py | 83 ++++++++++++++++ tests/client/test_get_all_flows.py | 146 ++++++++++++++++++++++++++++ tests/client/test_get_app_flavor.py | 2 +- tests/client/test_get_flows.py | 31 ++++++ 4 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 tests/client/test_get_all_flows.py create mode 100644 tests/client/test_get_flows.py diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 5c90bef..20949a5 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -9,6 +9,7 @@ HYPERNODE_API_APP_DETAIL_WITH_ADDONS_ENDPOINT = "/v2/app/{}/with_addons?destroyed=false" HYPERNODE_API_APP_EAV_DESCRIPTION_ENDPOINT = "/v2/app/eav_descriptions/" HYPERNODE_API_APP_FLAVOR_ENDPOINT = "/v2/app/{}/flavor/" +HYPERNODE_API_APP_FLOWS_ENDPOINT = "/logbook/v1/logbooks/{}/flows" HYPERNODE_API_BRANCHER_APP_ENDPOINT = "/v2/brancher/app/{}/" HYPERNODE_API_BRANCHER_ENDPOINT = "/v2/brancher/{}/" HYPERNODE_API_APP_NEXT_BEST_PLAN_ENDPOINT = "/v2/app/{}/next_best_plan/" @@ -169,6 +170,88 @@ def get_app_flavor(self, app_name): """ return self.requests("GET", HYPERNODE_API_APP_FLAVOR_ENDPOINT.format(app_name)) + def get_flows(self, app_name): + """ + List the flows for an app. Take note that this result is paginated and will not + retrieve all flows. If you are looking to retrieve all flows and not those available + on just the first page, look at the get_all_flows method instead. + + Example: + > client.get_flows('yourhypernodeappname').json() + > {'count': 2, + > 'next': None, + > 'previous': None, + > 'results': [{'uuid': '1d1b8437-c8c0-4e96-a28a-26cc311c1f4c', + > 'state': 'success', + > 'name': 'update_node', + > 'created_at': '2023-03-04T14:42:24Z', + > 'updated_at': '2023-03-04T14:42:57Z', + > 'progress': {'running': [], 'total': 8, 'completed': 8}, + > 'logbook': 'yourhypernodeappname', + > 'tracker': {'uuid': '6012bf2c-952d-4cb0-97ff-1022614b50b7', + > 'description': None}}, + > {'uuid': 'fe696417-024e-4a57-8442-4967f6df24a3', + > 'state': 'success', + > 'name': 'create_backup', + > 'created_at': '2023-03-04T14:13:00Z', + > 'updated_at': '2023-03-04T14:13:12Z', + > 'progress': {'running': [], 'total': 2, 'completed': 2}, + > 'logbook': 'yourhypernodeappname', + > 'tracker': {'uuid': None, 'description': None}}] + > } + + + :param str app_name: The name of the app to get the flows for + :return obj response: The request response object + """ + return self.requests("GET", HYPERNODE_API_APP_FLOWS_ENDPOINT.format(app_name)) + + def get_all_flows(self, app_name, limit=None): + """ + List all the flows for an app (paginate over the results), or + retrieve results until we have enough (if limit is specified). + Note that this method does not return a response object but a + list of dicts instead. + Example: + > client.get_all_flows('yourhypernodeappname') + > [{'uuid': '03bd6e10-5493-4ee8-92fc-cc429faebead', + > 'state': None, + > 'name': 'update_node', + > 'created_at': '2023-03-05T14:01:56Z', + > 'updated_at': None, + > 'progress': {'running': [], 'total': 0, 'completed': 0}, + > 'logbook': 'yourhypernodeappname', + > 'tracker': {'uuid': '0dd83d83-6b9b-4fb5-9665-79fcf8235069', + > 'description': None}}, + > {'uuid': 'ace36ab9-323e-4a95-a0c6-46f96945b623', + > 'state': None, + > 'name': 'update_node', + > 'created_at': '2023-03-05T14:01:55Z', + > 'updated_at': None, + > 'progress': {'running': [], 'total': 0, 'completed': 0}, + > 'logbook': 'yourhypernodeappname', + > 'tracker': {'uuid': '0006206e-df48-4af4-8e7b-b3db86d35bb0', + > 'description': None}}, + > ... + > ] + + :param str app_name: The name of the app to get all the flows for + :param int limit: How many flows to get. If None is specified all + available flows will be retrieved. + :return list flows: A list of all flows + """ + flows = [] + next_url = HYPERNODE_API_APP_FLOWS_ENDPOINT.format(app_name) + while next_url: + if limit and len(flows) >= limit: + break + response = self.requests("GET", next_url.replace(HYPERNODE_API_URL, "")) + response_data = response.json() + results = response_data["results"] + flows.extend(results) + next_url = response_data["next"] + return flows[:limit] + def get_slas(self): """ List the available SLAs diff --git a/tests/client/test_get_all_flows.py b/tests/client/test_get_all_flows.py new file mode 100644 index 0000000..ea87878 --- /dev/null +++ b/tests/client/test_get_all_flows.py @@ -0,0 +1,146 @@ +from unittest.mock import Mock, call + +from tests.testcase import TestCase +from hypernode_api_python.client import ( + HypernodeAPIPython, +) + + +class TestGetAllFlows(TestCase): + def setUp(self): + self.client = HypernodeAPIPython(token="mytoken") + self.mock_request = Mock() + self.client.requests = self.mock_request + self.flow1 = { + "uuid": "e1db2b60-882d-4b43-8910-ce6d38ca5393", + "state": None, + "name": "create_backup", + "created_at": "2023-03-05T14:13:21Z", + "updated_at": None, + "progress": {"running": [], "total": 0, "completed": 0}, + "logbook": "my_app", + "tracker": {"uuid": None, "description": None}, + } + self.flow2 = { + "uuid": "03bd6e10-5493-4ee8-92fc-cc429faebead", + "state": None, + "name": "update_node", + "created_at": "2023-03-05T14:01:56Z", + "updated_at": None, + "progress": {"running": [], "total": 0, "completed": 0}, + "logbook": "my_app", + "tracker": { + "uuid": "0dd83d83-6b9b-4fb5-9665-79fcf8235069", + "description": None, + }, + } + + def test_get_all_flows_returns_flows_if_only_one_page(self): + self.mock_request.return_value.json.return_value = { + "count": 2, + "next": None, + "previous": None, + "results": [self.flow1, self.flow2], + } + + ret = self.client.get_all_flows("my_app") + + expected_calls = [ + call("GET", "/logbook/v1/logbooks/my_app/flows"), + call().json(), + ] + self.assertEqual(expected_calls, self.mock_request.mock_calls) + expected_results = [self.flow1, self.flow2] + self.assertEqual(expected_results, ret) + + def test_get_all_flows_returns_flows_if_only_one_page_but_limited_results_requested( + self, + ): + self.mock_request.return_value.json.return_value = { + "count": 2, + "next": None, + "previous": None, + "results": [self.flow1, self.flow2], + } + + ret = self.client.get_all_flows("my_app", limit=1) + + expected_calls = [ + call("GET", "/logbook/v1/logbooks/my_app/flows"), + call().json(), + ] + self.assertEqual(expected_calls, self.mock_request.mock_calls) + expected_results = [self.flow1] + self.assertEqual(expected_results, ret) + + def test_get_all_flows_returns_flows_if_more_than_one_page(self): + self.mock_request.return_value.json.side_effect = [ + { + "count": 101, + "next": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=50", + "previous": None, + "results": [self.flow1, self.flow2] * 25, + }, + { + "count": 101, + "next": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=100", + "previous": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=50", + "results": [self.flow1, self.flow2] * 25, + }, + { + "count": 101, + "next": None, + "previous": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=100", + "results": [self.flow1], + }, + ] + + ret = self.client.get_all_flows("my_app") + + expected_calls = [ + call("GET", "/logbook/v1/logbooks/my_app/flows"), + call().json(), + call("GET", "/logbook/v1/logbooks/my_app/flows/?limit=50&offset=50"), + call().json(), + call("GET", "/logbook/v1/logbooks/my_app/flows/?limit=50&offset=100"), + call().json(), + ] + self.assertEqual(expected_calls, self.mock_request.mock_calls) + expected_results = [self.flow1, self.flow2] * 50 + [self.flow1] + self.assertEqual(expected_results, ret) + + def test_get_all_flows_returns_flows_if_more_than_one_page_but_limited_results_requested( + self, + ): + self.mock_request.return_value.json.side_effect = [ + { + "count": 101, + "next": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=50", + "previous": None, + "results": [self.flow1, self.flow2] * 25, + }, + { + "count": 101, + "next": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=100", + "previous": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=50", + "results": [self.flow1, self.flow2] * 25, + }, + { + "count": 101, + "next": None, + "previous": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=100", + "results": [self.flow1], + }, + ] + + ret = self.client.get_all_flows("my_app", limit=51) + + expected_calls = [ + call("GET", "/logbook/v1/logbooks/my_app/flows"), + call().json(), + call("GET", "/logbook/v1/logbooks/my_app/flows/?limit=50&offset=50"), + call().json(), + ] + self.assertEqual(expected_calls, self.mock_request.mock_calls) + expected_results = [self.flow1, self.flow2] * 25 + [self.flow1] + self.assertEqual(expected_results, ret) diff --git a/tests/client/test_get_app_flavor.py b/tests/client/test_get_app_flavor.py index 0b63704..3cdaa33 100644 --- a/tests/client/test_get_app_flavor.py +++ b/tests/client/test_get_app_flavor.py @@ -16,7 +16,7 @@ def setUp(self): def test_app_flavor_endpoint_is_correct(self): self.assertEqual("/v2/app/{}/flavor/", HYPERNODE_API_APP_FLAVOR_ENDPOINT) - def test_calls_app_flavor_endpoint_propertly(self): + def test_calls_app_flavor_endpoint_properly(self): self.client.get_app_flavor("my_app") self.mock_request.assert_called_once_with("GET", "/v2/app/my_app/flavor/") diff --git a/tests/client/test_get_flows.py b/tests/client/test_get_flows.py new file mode 100644 index 0000000..8fcb2d4 --- /dev/null +++ b/tests/client/test_get_flows.py @@ -0,0 +1,31 @@ +from unittest.mock import Mock + +from tests.testcase import TestCase +from hypernode_api_python.client import ( + HypernodeAPIPython, + HYPERNODE_API_APP_FLOWS_ENDPOINT, +) + + +class TestGetFlows(TestCase): + def setUp(self): + self.client = HypernodeAPIPython(token="mytoken") + self.mock_request = Mock() + self.client.requests = self.mock_request + + def test_flows_endpoint_is_correct(self): + self.assertEqual( + "/logbook/v1/logbooks/{}/flows", HYPERNODE_API_APP_FLOWS_ENDPOINT + ) + + def test_calls_flows_endpoint_properly(self): + self.client.get_flows("my_app") + + self.mock_request.assert_called_once_with( + "GET", "/logbook/v1/logbooks/my_app/flows" + ) + + def test_returns_flows_data(self): + self.assertEqual( + self.client.get_flows("my_app"), self.mock_request.return_value + ) From 66021f7cd280a0e09c2d03284c2cd68327878d1d Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Mon, 6 Mar 2023 09:47:42 +0100 Subject: [PATCH 24/38] add get block attack description to client looks like: ``` client.requests("GET", "/v2/app/block_attack_descriptions/").json() { 'BlockSqliBruteForce': 'Attempts to deploy NGINX rules to block suspected (blind) SQL injection attacks', ... } ``` --- hypernode_api_python/client.py | 23 ++++++++++-- .../test_get_block_attack_descriptions.py | 37 +++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 tests/client/test_get_block_attack_descriptions.py diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 20949a5..81a1aa0 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -10,19 +10,20 @@ HYPERNODE_API_APP_EAV_DESCRIPTION_ENDPOINT = "/v2/app/eav_descriptions/" HYPERNODE_API_APP_FLAVOR_ENDPOINT = "/v2/app/{}/flavor/" HYPERNODE_API_APP_FLOWS_ENDPOINT = "/logbook/v1/logbooks/{}/flows" -HYPERNODE_API_BRANCHER_APP_ENDPOINT = "/v2/brancher/app/{}/" -HYPERNODE_API_BRANCHER_ENDPOINT = "/v2/brancher/{}/" HYPERNODE_API_APP_NEXT_BEST_PLAN_ENDPOINT = "/v2/app/{}/next_best_plan/" +HYPERNODE_API_APP_ORDER_ENDPOINT = "/v2/app/order/" HYPERNODE_API_APP_PRODUCT_LIST_ENDPOINT = "/v2/product/app/{}/" HYPERNODE_API_APP_XGRADE_CHECK_ENDPOINT = "/v2/app/xgrade/{}/check/{}/" HYPERNODE_API_APP_XGRADE_ENDPOINT = "/v2/app/xgrade/{}/" HYPERNODE_API_BACKUPS_ENDPOINT = "/v2/app/{}/backup/" +HYPERNODE_API_BLOCK_ATTACK_DESCRIPTION_ENDPOINT = "/v2/app/block_attack_descriptions/" +HYPERNODE_API_BRANCHER_APP_ENDPOINT = "/v2/brancher/app/{}/" +HYPERNODE_API_BRANCHER_ENDPOINT = "/v2/brancher/{}/" HYPERNODE_API_PRODUCT_APP_DETAIL_ENDPOINT = "/v2/product/app/{}/current/" HYPERNODE_API_PRODUCT_LIST_ENDPOINT = "/v2/product/" HYPERNODE_API_PRODUCT_PRICE_DETAIL_ENDPOINT = "/v2/product/{}/with_price/" HYPERNODE_API_VALIDATE_APP_NAME_ENDPOINT = "/v2/app/name/validate/" HYPERNODE_API_WHITELIST_ENDPOINT = "/v2/whitelist/{}/" -HYPERNODE_API_APP_ORDER_ENDPOINT = "/v2/app/order/" class HypernodeAPIPython: @@ -404,6 +405,22 @@ def get_product_info_with_price(self, product_code, error_to_raise=None): raise error_to_raise return response + def get_block_attack_descriptions(self): + """ + Get the block attack descriptions. These can be used to block attacks + by automatically placing an NGINX snippet in /data/web/nginx if the + specified block is compatible with the current NGINX configuration. + Example: + > client.requests("GET", "/v2/app/block_attack_descriptions/").json() + > { + > 'BlockSqliBruteForce': 'Attempts to deploy NGINX rules to block suspected (blind) SQL injection attack', + > ... + > } + + :return obj response: The request response object + """ + return self.requests("GET", HYPERNODE_API_BLOCK_ATTACK_DESCRIPTION_ENDPOINT) + def get_whitelist_options(self, app_name): """ Get whitelist options for app. Retrieve the options for specifying diff --git a/tests/client/test_get_block_attack_descriptions.py b/tests/client/test_get_block_attack_descriptions.py new file mode 100644 index 0000000..ebd3275 --- /dev/null +++ b/tests/client/test_get_block_attack_descriptions.py @@ -0,0 +1,37 @@ +from unittest.mock import Mock + +from tests.testcase import TestCase +from hypernode_api_python.client import ( + HypernodeAPIPython, + HYPERNODE_API_BLOCK_ATTACK_DESCRIPTION_ENDPOINT, +) + + +class TestGetBlockAttackDescriptions(TestCase): + def setUp(self): + self.client = HypernodeAPIPython(token="mytoken") + self.mock_request = Mock() + self.client.requests = self.mock_request + + def test_block_attack_descriptions_endpoint_is_correct(self): + self.assertEqual( + "/v2/app/block_attack_descriptions/", + HYPERNODE_API_BLOCK_ATTACK_DESCRIPTION_ENDPOINT, + ) + + def test_calls_block_attack_descriptions_detail_endpoint_properly(self): + self.client.get_block_attack_descriptions() + + self.mock_request.assert_called_once_with( + "GET", "/v2/app/block_attack_descriptions/" + ) + + def test_returns_block_attack_descriptions(self): + response = Mock() + response.status_code = 200 + self.mock_request.return_value = response + + self.assertEqual( + self.client.get_block_attack_descriptions(), + self.mock_request.return_value, + ) From 33e45978e9c6336e9d46530cf19164e9d8ab2b0b Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Tue, 7 Mar 2023 09:10:06 +0100 Subject: [PATCH 25/38] add method for blocking various attacks looks like: ``` client.block_attack('yourhypernodeappname', 'BlockSqliBruteForce').ok True ``` --- hypernode_api_python/client.py | 24 ++++++++++++++++++- tests/client/test_block_attack.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/client/test_block_attack.py diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 81a1aa0..9b3aa98 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -16,6 +16,7 @@ HYPERNODE_API_APP_XGRADE_CHECK_ENDPOINT = "/v2/app/xgrade/{}/check/{}/" HYPERNODE_API_APP_XGRADE_ENDPOINT = "/v2/app/xgrade/{}/" HYPERNODE_API_BACKUPS_ENDPOINT = "/v2/app/{}/backup/" +HYPERNODE_API_BLOCK_ATTACK_ENDPOINT = "/v2/app/{}/block_attack/" HYPERNODE_API_BLOCK_ATTACK_DESCRIPTION_ENDPOINT = "/v2/app/block_attack_descriptions/" HYPERNODE_API_BRANCHER_APP_ENDPOINT = "/v2/brancher/app/{}/" HYPERNODE_API_BRANCHER_ENDPOINT = "/v2/brancher/{}/" @@ -411,7 +412,7 @@ def get_block_attack_descriptions(self): by automatically placing an NGINX snippet in /data/web/nginx if the specified block is compatible with the current NGINX configuration. Example: - > client.requests("GET", "/v2/app/block_attack_descriptions/").json() + > client.get_block_attack_descriptions().json() > { > 'BlockSqliBruteForce': 'Attempts to deploy NGINX rules to block suspected (blind) SQL injection attack', > ... @@ -421,6 +422,27 @@ def get_block_attack_descriptions(self): """ return self.requests("GET", HYPERNODE_API_BLOCK_ATTACK_DESCRIPTION_ENDPOINT) + def block_attack(self, app_name, attack_name): + """ + Attempts to deploy one of the block attack rules. The list of rules can be retrieved with the + get_block_attack_descriptions method. This will place an NGINX snippet to block the specified attack + in /data/web/nginx. If the nginx-config-validator detects that the new config is valid, it will leave + the newly placed config. Otherwise it will be removed again. + + Example: + > client.block_attack('yourhypernodeappname', 'BlockSqliBruteForce').ok + > True + + :param str app_name: The name of the Hypernode you want to deploy the block attack nginx configuration on + :param str attack_name: The specific attack you want to block. See get_block_attack_descriptions for options. + :return obj response: The request response object + """ + return self.requests( + "POST", + HYPERNODE_API_BLOCK_ATTACK_ENDPOINT.format(app_name), + data={"attack_name": attack_name}, + ) + def get_whitelist_options(self, app_name): """ Get whitelist options for app. Retrieve the options for specifying diff --git a/tests/client/test_block_attack.py b/tests/client/test_block_attack.py new file mode 100644 index 0000000..aef443e --- /dev/null +++ b/tests/client/test_block_attack.py @@ -0,0 +1,39 @@ +from unittest import TestCase +from unittest.mock import Mock + +from hypernode_api_python.client import ( + HYPERNODE_API_BLOCK_ATTACK_ENDPOINT, + HypernodeAPIPython, +) + + +class TestBlockAttack(TestCase): + def setUp(self): + self.client = HypernodeAPIPython(token="my_token") + self.mock_request = Mock() + self.client.requests = self.mock_request + self.app_name = "my_app" + + def test_block_attack_endpoint_is_correct(self): + self.assertEqual( + "/v2/app/{}/block_attack/", HYPERNODE_API_BLOCK_ATTACK_ENDPOINT + ) + + def test_calls_block_attack_endpoint_correctly(self): + self.client.block_attack(self.app_name, "BlockSqliBruteForce") + + self.mock_request.assert_called_once_with( + "POST", + f"/v2/app/{self.app_name}/block_attack/", + data={"attack_name": "BlockSqliBruteForce"}, + ) + + def test_returns_block_attack_data(self): + response = Mock() + response.status_code = 202 + self.mock_request.return_value = response + + self.assertEqual( + self.client.block_attack(self.app_name, "BlockSqliBruteForce"), + self.mock_request.return_value, + ) From 38110d08e0576ed6d7afccf5ada4c58d55a8dd34 Mon Sep 17 00:00:00 2001 From: alejandro Date: Fri, 10 Mar 2023 06:27:28 +0100 Subject: [PATCH 26/38] feat: update hypernode-api-python version to 0.0.6 --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aa32798..945389f 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ $ python3 -m venv venv $ . venv/bin/activate $ pip install hypernode-api-python $ pip freeze | grep hypernode-api-python -hypernode-api-python==0.0.5 +hypernode-api-python==0.0.6 ``` ### Performing API calls diff --git a/setup.py b/setup.py index d319956..6728a59 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( name="hypernode_api_python", - version="0.0.5", + version="0.0.6", description='"Hypernode API Client for Python"', url="https://github.com/ByteInternet/hypernode_api_python", packages=find_packages( From 8cb5203c468d4d0d9056b5bec196e763d83c20e3 Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Fri, 24 May 2024 16:51:49 +0200 Subject: [PATCH 27/38] add get_cluster_relations to api client looks like: ``` In [1]: from hypernode_api_python.client import HypernodeAPIPython In [2]: import os In [3]: client = HypernodeAPIPython(os.environ['MYSECRETAPITOKEN']) In [4]: client.get_cluster_relations("mytestappweb").json() Out[4]: {'parents': [{'id': 182, 'parent': 'mytestappdb', 'child': 'mytestappweb', 'relation_type': 'mysql', 'cluster_description': None}, {'id': 180, 'parent': 'mytestapp', 'child': 'mytestappweb', 'relation_type': 'loadbalancer', 'cluster_description': None}, {'id': 181, 'parent': 'mytestapp', 'child': 'mytestappweb', 'relation_type': 'nfs', 'cluster_description': None}], 'children': []} ``` --- hypernode_api_python/client.py | 29 ++++++++++++++++++++++ tests/client/test_get_cluster_relations.py | 28 +++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 tests/client/test_get_cluster_relations.py diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 9b3aa98..4bfbb13 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -5,6 +5,7 @@ HYPERNODE_API_ADDON_SLA_LIST_ENDPOINT = "/v2/addon/slas/" HYPERNODE_API_APP_CHECK_PAYMENT_INFORMATION = "/v2/app/{}/check-payment-information/" HYPERNODE_API_APP_CONFIGURATION_ENDPOINT = "/v2/configuration/" +HYPERNODE_API_APP_CLUSTER_RELATIONS = "/v2/app/{}/relations/" HYPERNODE_API_APP_DETAIL_ENDPOINT = "/v2/app/{}/?destroyed=false" HYPERNODE_API_APP_DETAIL_WITH_ADDONS_ENDPOINT = "/v2/app/{}/with_addons?destroyed=false" HYPERNODE_API_APP_EAV_DESCRIPTION_ENDPOINT = "/v2/app/eav_descriptions/" @@ -365,6 +366,34 @@ def get_app_configurations(self): """ return self.requests("GET", HYPERNODE_API_APP_CONFIGURATION_ENDPOINT) + def get_cluster_relations(self, app_name): + """ " + List all relations for the specified app. This will return all the + relations that are currently configured for the specified app. + + Example: + > client.get_cluster_relations('mytestappweb').json() + > {'children': [], + > 'parents': [{'child': 'mytestappweb', + > 'cluster_description': None, + > 'id': 182, + > 'parent': 'mytestappdb', + > 'relation_type': 'mysql'}, + > {'child': 'mytestappweb', + > 'cluster_description': None, + > 'id': 180, + > 'parent': 'mytestapp', + > 'relation_type': 'loadbalancer'}, + > {'child': 'mytestappweb', + > 'cluster_description': None, + > 'id': 181, + > 'parent': 'mytestapp', + > 'relation_type': 'nfs'}]} + """ + return self.requests( + "GET", HYPERNODE_API_APP_CLUSTER_RELATIONS.format(app_name) + ) + def get_product_info_with_price(self, product_code, error_to_raise=None): """ Get information about a specific product diff --git a/tests/client/test_get_cluster_relations.py b/tests/client/test_get_cluster_relations.py new file mode 100644 index 0000000..4b5b7ec --- /dev/null +++ b/tests/client/test_get_cluster_relations.py @@ -0,0 +1,28 @@ +from unittest.mock import Mock + +from tests.testcase import TestCase +from hypernode_api_python.client import ( + HypernodeAPIPython, + HYPERNODE_API_APP_CLUSTER_RELATIONS, +) + + +class TestGetClusterRelations(TestCase): + def setUp(self): + self.mock_request = Mock() + self.client = HypernodeAPIPython(token="mytoken") + self.client.requests = self.mock_request + + def test_calls_hypernode_api_cluster_relations_endpoint_with_correct_parameters( + self, + ): + self.client.get_cluster_relations("yourhypernodeappname") + + self.mock_request.assert_called_once_with( + "GET", HYPERNODE_API_APP_CLUSTER_RELATIONS.format("yourhypernodeappname") + ) + + def test_returns_result_for_hypernode_api_cluster_relations(self): + ret = self.client.get_cluster_relations("yourhypernodeappname") + + self.assertEqual(ret, self.mock_request.return_value) From fb17ebb33b70bf48b45804624b7f02d20d5aabec Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Fri, 24 May 2024 17:51:21 +0200 Subject: [PATCH 28/38] update code owners --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 90e16ad..1b08999 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @vdloo @AlexanderGrooff @alejandrogarza @pieterbeens @tdgroot @markdentoom @martijneichelsheim +* @vdloo @alejandrogarza @tdgroot @martijneichelsheim From 992dd5380becbb6dc5d4dbf6dd409087e2177fb6 Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Sat, 25 May 2024 08:10:45 +0200 Subject: [PATCH 29/38] implement get_sla method for getting a specific SLA --- hypernode_api_python/client.py | 17 +++++++++++++++++ tests/client/test_get_sla.py | 26 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 tests/client/test_get_sla.py diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 4bfbb13..c35d874 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -2,6 +2,7 @@ DEFAULT_USER_AGENT = "hypernode-api-python" HYPERNODE_API_URL = "https://api.hypernode.com" +HYPERNODE_API_ADDON_LIST_ENDPOINT = "/v2/addon/" HYPERNODE_API_ADDON_SLA_LIST_ENDPOINT = "/v2/addon/slas/" HYPERNODE_API_APP_CHECK_PAYMENT_INFORMATION = "/v2/app/{}/check-payment-information/" HYPERNODE_API_APP_CONFIGURATION_ENDPOINT = "/v2/configuration/" @@ -272,6 +273,22 @@ def get_slas(self): """ return self.requests("GET", HYPERNODE_API_ADDON_SLA_LIST_ENDPOINT) + def get_sla(self, sla_code): + """ + List a specific SLA + Example: + > client.get_sla("sla-standard").json() + > {'billing_period': 1, + > 'billing_period_unit': 'month', + > 'code': 'sla-standard', + > 'id': 123, + > 'name': 'SLA Standard', + > 'price': 1234} + + :return obj response: The request response object + """ + return self.requests("GET", HYPERNODE_API_ADDON_LIST_ENDPOINT + sla_code + "/") + def get_available_backups_for_app(self, app_name): """ Lists the available backups for the specified app diff --git a/tests/client/test_get_sla.py b/tests/client/test_get_sla.py new file mode 100644 index 0000000..02a0487 --- /dev/null +++ b/tests/client/test_get_sla.py @@ -0,0 +1,26 @@ +from unittest.mock import Mock + +from tests.testcase import TestCase +from hypernode_api_python.client import ( + HypernodeAPIPython, + HYPERNODE_API_ADDON_LIST_ENDPOINT, +) + + +class TestGetSla(TestCase): + def setUp(self): + self.mock_request = Mock() + self.client = HypernodeAPIPython(token="mytoken") + self.client.requests = self.mock_request + + def test_calls_hypernode_api_endpoint_with_correct_parameters(self): + self.client.get_sla("sla-standard") + + self.mock_request.assert_called_once_with( + "GET", HYPERNODE_API_ADDON_LIST_ENDPOINT + "sla-standard/" + ) + + def test_returns_json_result(self): + ret = self.client.get_sla("sla-standard") + + self.assertEqual(ret, self.mock_request.return_value) From 91aaa9ea878791b7e01c5d0a0cc1ff0647a6223d Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Sat, 25 May 2024 13:38:34 +0200 Subject: [PATCH 30/38] add simple cli entrypoints for api functionality like: ``` $ ls bin/ block_attack get_app_flavor get_block_attack_descriptions get_flows get_sla get_whitelist_rules command get_app_info get_cluster_relations get_next_best_plan_for_app get_slas validate_app_name get_app_configurations get_available_backups_for_app get_eav_description get_product_info get_whitelist_options hypernode-api-python]$ ./bin/get_sla --help usage: get_sla [-h] sla_code Get a specific SLA. $ ./bin/get_sla sla-standard { "id": 123, "code": "sla-standard", "name": "SLA Standard", "price": 1234, "billing_period": 1, "billing_period_unit": "month" } positional arguments: sla_code The code of the SLA to get optional arguments: -h, --help show this help message and exit ``` --- .github/workflows/test.yaml | 2 +- README.md | 32 +- bin/block_attack | 1 + bin/command | 14 + bin/get_app_configurations | 1 + bin/get_app_flavor | 1 + bin/get_app_info | 1 + bin/get_available_backups_for_app | 1 + bin/get_block_attack_descriptions | 1 + bin/get_cluster_relations | 1 + bin/get_eav_description | 1 + bin/get_flows | 1 + bin/get_next_best_plan_for_app | 1 + bin/get_product_info | 1 + bin/get_sla | 1 + bin/get_slas | 1 + bin/get_whitelist_options | 1 + bin/get_whitelist_rules | 1 + bin/validate_app_name | 1 + hypernode_api_python/client.py | 12 +- hypernode_api_python/commands.py | 479 ++++++++++++++++++ tests/commands/__init__.py | 0 tests/commands/test_block_attack.py | 54 ++ tests/commands/test_get_app_configurations.py | 28 + tests/commands/test_get_app_flavor.py | 37 ++ tests/commands/test_get_app_info.py | 37 ++ tests/commands/test_get_app_name.py | 29 ++ .../test_get_available_backups_for_app.py | 37 ++ .../test_get_block_attack_descriptions.py | 28 + tests/commands/test_get_client.py | 37 ++ tests/commands/test_get_cluster_relations.py | 37 ++ tests/commands/test_get_eav_description.py | 28 + tests/commands/test_get_flows.py | 35 ++ tests/commands/test_get_next_best_plan.py | 39 ++ tests/commands/test_get_product_info.py | 30 ++ tests/commands/test_get_sla.py | 26 + tests/commands/test_get_slas.py | 26 + tests/commands/test_get_whitelist_options.py | 37 ++ tests/commands/test_get_whitelist_rules.py | 37 ++ tests/commands/test_print_response.py | 32 ++ tests/commands/test_validate_app_name.py | 55 ++ tox.ini | 4 +- 42 files changed, 1224 insertions(+), 4 deletions(-) create mode 120000 bin/block_attack create mode 100755 bin/command create mode 120000 bin/get_app_configurations create mode 120000 bin/get_app_flavor create mode 120000 bin/get_app_info create mode 120000 bin/get_available_backups_for_app create mode 120000 bin/get_block_attack_descriptions create mode 120000 bin/get_cluster_relations create mode 120000 bin/get_eav_description create mode 120000 bin/get_flows create mode 120000 bin/get_next_best_plan_for_app create mode 120000 bin/get_product_info create mode 120000 bin/get_sla create mode 120000 bin/get_slas create mode 120000 bin/get_whitelist_options create mode 120000 bin/get_whitelist_rules create mode 120000 bin/validate_app_name create mode 100644 hypernode_api_python/commands.py create mode 100644 tests/commands/__init__.py create mode 100644 tests/commands/test_block_attack.py create mode 100644 tests/commands/test_get_app_configurations.py create mode 100644 tests/commands/test_get_app_flavor.py create mode 100644 tests/commands/test_get_app_info.py create mode 100644 tests/commands/test_get_app_name.py create mode 100644 tests/commands/test_get_available_backups_for_app.py create mode 100644 tests/commands/test_get_block_attack_descriptions.py create mode 100644 tests/commands/test_get_client.py create mode 100644 tests/commands/test_get_cluster_relations.py create mode 100644 tests/commands/test_get_eav_description.py create mode 100644 tests/commands/test_get_flows.py create mode 100644 tests/commands/test_get_next_best_plan.py create mode 100644 tests/commands/test_get_product_info.py create mode 100644 tests/commands/test_get_sla.py create mode 100644 tests/commands/test_get_slas.py create mode 100644 tests/commands/test_get_whitelist_options.py create mode 100644 tests/commands/test_get_whitelist_rules.py create mode 100644 tests/commands/test_print_response.py create mode 100644 tests/commands/test_validate_app_name.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c20f142..b36aa48 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 945389f..48bd049 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,36 @@ pip install -r requirements/development.txt Each Hypernode has an API token associated with it, you can use that to talk to the API directly. You can find the token in `/etc/hypernode/hypernode_api_token`. For API tokens with special permissions please contact support@hypernode.com. Not all functionality in the API is currently generally available but if you'd like to start automating and have an interesting use-case we'd love to hear from you. +## Using the library from the commandline + +In the `bin/` directory you'll find entry points to interact with the API directly from the commandline. + +See for example: +```bash +$ export PYTHONPATH=. + +$ ./bin/get_slas --help +usage: get_slas [-h] + +List all available SLAs. + +Example: +$ ./bin/get_slas +[ + { + "id": 123, + "code": "sla-standard", + "name": "SLA Standard", + "price": 1234, + "billing_period": 1, + "billing_period_unit": "month" + }, + ... +] + +optional arguments: + -h, --help show this help message and exit +``` ### Installing the library in your project @@ -56,7 +86,7 @@ client.set_app_setting('yourhypernodeappname', 'php_version', '8.1').json() {'name': 'yourhypernodeappname', 'type': 'persistent', 'php_version': '8.1', ...} ``` -To even performing acts of cloud automation, like scaling to the first next larger plan: +You can even perform acts of cloud automation, like scaling to the first next larger plan: ```python client.xgrade( 'yourhypernodeappname', diff --git a/bin/block_attack b/bin/block_attack new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/block_attack @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/command b/bin/command new file mode 100755 index 0000000..3d67aa5 --- /dev/null +++ b/bin/command @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +import os +import sys +from hypernode_api_python import commands + + +if __name__ == '__main__': + command = os.path.basename(__file__) + + if hasattr(commands, command): + sys.exit(getattr(commands, command)() or 0) + else: + sys.stderr.write("Command '{}' not found in hypernode_api_python.commands\n".format(command)) + sys.exit(1) diff --git a/bin/get_app_configurations b/bin/get_app_configurations new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_app_configurations @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_app_flavor b/bin/get_app_flavor new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_app_flavor @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_app_info b/bin/get_app_info new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_app_info @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_available_backups_for_app b/bin/get_available_backups_for_app new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_available_backups_for_app @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_block_attack_descriptions b/bin/get_block_attack_descriptions new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_block_attack_descriptions @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_cluster_relations b/bin/get_cluster_relations new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_cluster_relations @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_eav_description b/bin/get_eav_description new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_eav_description @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_flows b/bin/get_flows new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_flows @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_next_best_plan_for_app b/bin/get_next_best_plan_for_app new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_next_best_plan_for_app @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_product_info b/bin/get_product_info new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_product_info @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_sla b/bin/get_sla new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_sla @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_slas b/bin/get_slas new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_slas @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_whitelist_options b/bin/get_whitelist_options new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_whitelist_options @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_whitelist_rules b/bin/get_whitelist_rules new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_whitelist_rules @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/validate_app_name b/bin/validate_app_name new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/validate_app_name @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index c35d874..b757fcb 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -331,6 +331,7 @@ def get_app_eav_description(self): """ return self.requests("GET", HYPERNODE_API_APP_EAV_DESCRIPTION_ENDPOINT) + # TODO: add entrypoint for this method in bin/ and commands.py def set_app_setting(self, app_name, attribute, value): """ Update a setting on the app, like the PHP or MySQL version. See @@ -384,7 +385,7 @@ def get_app_configurations(self): return self.requests("GET", HYPERNODE_API_APP_CONFIGURATION_ENDPOINT) def get_cluster_relations(self, app_name): - """ " + """ List all relations for the specified app. This will return all the relations that are currently configured for the specified app. @@ -579,6 +580,7 @@ def get_whitelist_rules(self, app_name, filter_data=None): "GET", HYPERNODE_API_WHITELIST_ENDPOINT.format(app_name), filter_data ) + # TODO: add entrypoint for this method in bin/ and commands.py def get_current_product_for_app(self, app_name): """ Retrieve information about the product the specified App is currently on. @@ -647,6 +649,7 @@ def get_current_product_for_app(self, app_name): "GET", HYPERNODE_API_PRODUCT_APP_DETAIL_ENDPOINT.format(app_name) ) + # TODO: add entrypoint for this method in bin/ and commands.py def check_payment_information_for_app(self, app_name): """ Get the payment information that is currently configured for this Hypernode @@ -664,6 +667,7 @@ def check_payment_information_for_app(self, app_name): "GET", HYPERNODE_API_APP_CHECK_PAYMENT_INFORMATION.format(app_name) ) + # TODO: add entrypoint for this method in bin/ and commands.py def get_active_products(self): """ Retrieve the list of products that are currently available. You can @@ -731,6 +735,7 @@ def get_active_products(self): """ return self.requests("GET", HYPERNODE_API_PRODUCT_LIST_ENDPOINT) + # TODO: add entrypoint for this method in bin/ and commands.py def check_xgrade(self, app_name, product_code): """ Checks if the Hypernode 'is going to fit' on the new product. Retrieves some @@ -759,6 +764,7 @@ def check_xgrade(self, app_name, product_code): HYPERNODE_API_APP_XGRADE_CHECK_ENDPOINT.format(app_name, product_code), ) + # TODO: add entrypoint for this method in bin/ and commands.py def xgrade(self, app_name, data): """ Change the product of a Hypernode to a different plan. This will initiate @@ -780,6 +786,7 @@ def xgrade(self, app_name, data): "PATCH", HYPERNODE_API_APP_XGRADE_ENDPOINT.format(app_name), data=data ) + # TODO: add entrypoint for this method in bin/ and commands.py def order_hypernode(self, data): """ Orders a new Hypernode. Note that you can not do this with the API permissions @@ -799,6 +806,7 @@ def order_hypernode(self, data): """ return self.requests("POST", HYPERNODE_API_APP_ORDER_ENDPOINT, data=data) + # TODO: add entrypoint for this method in bin/ and commands.py def get_active_branchers(self, app_name): """ List all active brancher nodes of your Hypernode. @@ -838,6 +846,7 @@ def get_active_branchers(self, app_name): "GET", HYPERNODE_API_BRANCHER_APP_ENDPOINT.format(app_name) ) + # TODO: add entrypoint for this method in bin/ and commands.py def create_brancher(self, app_name, data): """ Create a new branch (server replica) of your Hypernode. @@ -851,6 +860,7 @@ def create_brancher(self, app_name, data): "POST", HYPERNODE_API_BRANCHER_APP_ENDPOINT.format(app_name), data=data ) + # TODO: add entrypoint for this method in bin/ and commands.py def destroy_brancher(self, brancher_name): """ Destroy an existing brancher node of your Hypernode. diff --git a/hypernode_api_python/commands.py b/hypernode_api_python/commands.py new file mode 100644 index 0000000..c945fe4 --- /dev/null +++ b/hypernode_api_python/commands.py @@ -0,0 +1,479 @@ +import json +from os import environ, EX_OK, EX_UNAVAILABLE +from argparse import ArgumentParser, RawTextHelpFormatter +from hypernode_api_python.client import HypernodeAPIPython + + +def get_client(): + """ + Instantiates the HypernodeAPIPython client with the token from the environment. + :return obj HypernodeAPIPython: The instantiated client + """ + api_token = environ.get("HYPERNODE_API_TOKEN") + if not api_token: + raise ValueError( + "HYPERNODE_API_TOKEN environment variable not set. " + "Try running `export HYPERNODE_API_TOKEN=yourapitoken`" + ) + client = HypernodeAPIPython(environ["HYPERNODE_API_TOKEN"]) + return client + + +def get_app_name(): + """ + Gets the app name from the environment. + :return str app_name: The app name + """ + app_name = environ.get("HYPERNODE_APP_NAME") + if not app_name: + raise ValueError( + "HYPERNODE_APP_NAME environment variable not set. " + "Try running `export HYPERNODE_APP_NAME=yourhypernodeappname`" + ) + return app_name + + +def print_response(response): + """ + Pretty prints the JSON response. + :param obj response: The response object + :return NoneType None: None + """ + print(json.dumps(response.json(), indent=2)) + + +def get_app_info(args=None): + parser = ArgumentParser( + description=""" +Get information about the Hypernode app. + +Example: +$ ./bin/get_app_info +{ + "name": "yourhypernodeappname", + "type": "persistent", + "product": { + "code": "FALCON_M_202203", + "name": "Falcon M", + "backups_enabled": true, + "storage_size_in_gb": 75, + "price": 1234, + "is_development": false, + "provider": "combell", + "varnish_supported": true, + "supports_sla": true + }, + "domainname": "yourhypernodeappname.hypernode.io", + ... +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.get_app_info_or_404(app_name)) + + +def get_next_best_plan_for_app(args=None): + parser = ArgumentParser( + description=""" +Get the plan that is the first bigger plan than the current plan for this Hypernode. +This is convenient for if you want to upgrade your Hypernode to a bigger plan using the +xgrade feature and you need to know which plan to upgrade to. + +Example: +$ ./bin/get_next_best_plan_for_app +{ + "code": "FALCON_L_202203", + "name": "Falcon L" +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.get_next_best_plan_for_app_or_404(app_name)) + + +def validate_app_name(args=None): + parser = ArgumentParser( + description=""" +Check if the specified app_name is valid and available. An app name can not be +registered if it's already taken. Also there are certain restrictions on the +app name, like it can't contain certain characters or exceed a certain length. + +Examples: +$ ./bin/validate_app_name hypernode +App name 'hypernode' is valid. + +$ ./bin/validate_app_name hyper_node +App name 'hyper_node' is invalid: ["This value can only contain non-capital letters 'a' through 'z' or digits 0 through 9."] +""", + formatter_class=RawTextHelpFormatter, + ) + parser.add_argument( + "app_name", + help="The app name to validate", + ) + args = parser.parse_args(args=args) + client = get_client() + try: + client.validate_app_name(args.app_name) + print("App name '{}' is valid.".format(args.app_name)) + exit(EX_OK) + except Exception as e: + print("App name '{}' is invalid: {}".format(args.app_name, e)) + exit(EX_UNAVAILABLE) + + +def get_app_flavor(args=None): + parser = ArgumentParser( + description=""" +Get the current flavor of the Hypernode app. + +Example: +$ ./bin/get_app_flavor +{ + "name": "3CPU/16GB/80GB (Falcon M 202202)", + "redis_size": "2048" +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.get_app_flavor(app_name)) + + +def get_flows(args=None): + parser = ArgumentParser( + description=""" +Llist the flows for the Hypernode app. This is a history of all the +Hypernode automation jobs that have been ran for this Hypernode. + +Example: +$ ./bin/get_flows +{ + "count": 36, + "next": null, + "previous": null, + "results": [ + { + "uuid": "ad3b520e-a424-4bcb-a6fb-7d1fc055c3fa", + "state": "success", + "name": "create_backup", + "created_at": "2024-05-25T10:01:25Z", + "updated_at": "2024-05-25T10:01:57Z", + "progress": { + "running": [], + "total": 2, + "completed": 2 + }, + "logbook": "myhypernodeappname", + "tracker": { + "uuid": null, + "description": null + } + }, + ... +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.get_flows(app_name)) + + +def get_slas(args=None): + parser = ArgumentParser( + description=""" +List all available SLAs. + +Example: +$ ./bin/get_slas +[ + { + "id": 123, + "code": "sla-standard", + "name": "SLA Standard", + "price": 1234, + "billing_period": 1, + "billing_period_unit": "month" + }, + ... +] +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + print_response(client.get_slas()) + + +def get_sla(args=None): + parser = ArgumentParser( + description=""" +Get a specific SLA. + +$ ./bin/get_sla sla-standard +{ + "id": 123, + "code": "sla-standard", + "name": "SLA Standard", + "price": 1234, + "billing_period": 1, + "billing_period_unit": "month" +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.add_argument("sla_code", help="The code of the SLA to get") + args = parser.parse_args(args=args) + client = get_client() + print_response(client.get_sla(args.sla_code)) + + +def get_available_backups_for_app(args=None): + parser = ArgumentParser( + description=""" +List the available backups for the Hypernode + +Example: +$ ./bin/get_available_backups_for_app +{ + "count": 10, + "next": null, + "previous": null, + "results": [ + { + "backup_created_at": "2024-05-02T12:00:33+02:00", + "type": "periodic", + "backup_id": "1169e792-8b05-449c-a7b1-7d52cf43153a", + "expired_at": "2024-05-30T12:00:33+02:00" + }, + ... +] +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.get_available_backups_for_app(app_name)) + + +def get_eav_description(args=None): + parser = ArgumentParser( + description=""" +List all available EAV settings and their descriptions. + +Example: +$ ./bin/get_eav_description +{ + "supervisor_enabled": [ + true, + false + ], + "redis_eviction_policy": [ + "noeviction", + "allkeys-lru", + "allkeys-lfu", + "volatile-lru", + "volatile-lfu", + "allkeys-random", + "volatile-random", + "volatile-ttl" + ], + ... +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + print_response(client.get_app_eav_description()) + + +def get_app_configurations(args=None): + parser = ArgumentParser( + description=""" +List all the available configurations that can be selected when ordering a new Hypernode. + +Example: +$ ./bin/get_app_configurations +{ + "count": 7, + "next": null, + "previous": null, + "results": [ + { + "name": "Akeneo 6.0", + "configuration_id": "akeneo_6_0", + "php_version": "8.0", + "mysql_version": "8.0", + ... + }, + ... +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + print_response(client.get_app_configurations()) + + +def get_cluster_relations(args=None): + parser = ArgumentParser( + description=""" +List all the cluster relations for the Hypernode. + +Example: +$ ./bin/get_cluster_relations +{ + "parents": [ + { + "id": 182, + "parent": "mytestappdb", + "child": "mytestappweb", + "relation_type": "mysql", + "cluster_description": null + }, + { + "id": 180, + "parent": "mytestapp", + "child": "mytestappweb", + "relation_type": "loadbalancer", + "cluster_description": null + }, + ... +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.get_cluster_relations(app_name)) + + +def get_product_info(args=None): + parser = ArgumentParser( + description=""" +Gets the product info for the specified product + +$ ./bin/get_product_info FALCON_S_202203 +{ + "code": "FALCON_S_202203", + "name": "Falcon S", + "backups_enabled": true, + "storage_size_in_gb": 57, + "price": 1234, + ... +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.add_argument("product_code", help="The code of the product to get") + args = parser.parse_args(args=args) + client = get_client() + print_response(client.get_product_info_with_price(args.product_code)) + + +def get_block_attack_descriptions(args=None): + parser = ArgumentParser( + description=""" +List all attack blocking strategies and their descriptions. + +Example: +$ ./bin/get_block_attack_descriptions +{ + "BlockSqliBruteForce": "Attempts to deploy NGINX rules to block suspected (blind) SQL injection attacks", + ... +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + print_response(client.get_block_attack_descriptions()) + + +def block_attack(args=None): + parser = ArgumentParser( + description=""" +Block a specific attack based on a pre-defined attack blocking strategy. + +$ ./bin/block_attack BlockSqliBruteForce +A job to block the 'BlockSqliBruteForce' attack has been posted. +""", + formatter_class=RawTextHelpFormatter, + ) + client = get_client() + choices = client.get_block_attack_descriptions().json().keys() + parser.add_argument("attack_name", help="The attack to block", choices=choices) + args = parser.parse_args(args=args) + app_name = get_app_name() + output = client.block_attack(app_name, args.attack_name).content + if output: + print(output) + else: + print( + "A job to block the '{}' attack has been posted.".format(args.attack_name) + ) + + +def get_whitelist_options(args=None): + parser = ArgumentParser( + description=""" +List all available WAF whitelist options for the Hypernode. + +Example: +$ ./bin/get_whitelist_options +{ + "name": "Whitelist", + "description": "", + "renders": [ + "application/json" + ], + ... +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.get_whitelist_options(app_name)) + + +def get_whitelist_rules(args=None): + parser = ArgumentParser( + description=""" +List all currently configured WAF whitelist rules for the Hypernode. + +Example: +$ ./bin/get_whitelist_rules +[ + { + "id": 1234, + "created": "2024-05-25T13:39:48Z", + "domainname": "yourhypernodeappname.hypernode.io", + "ip": "1.2.3.4", + "type": "database", + "description": "my description" + }, + ... +] +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.get_whitelist_rules(app_name)) diff --git a/tests/commands/__init__.py b/tests/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/commands/test_block_attack.py b/tests/commands/test_block_attack.py new file mode 100644 index 0000000..270ca49 --- /dev/null +++ b/tests/commands/test_block_attack.py @@ -0,0 +1,54 @@ +from unittest.mock import Mock + +from hypernode_api_python.commands import block_attack +from tests.testcase import TestCase + + +class TestBlockAttack(TestCase): + def setUp(self): + self.print = self.set_up_patch("hypernode_api_python.commands.print") + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + self.client.get_block_attack_descriptions.return_value = Mock( + json=lambda: {"BlockSqliBruteForce": "", "BlockDownloaderBruteForce": ""} + ) + self.client.block_attack.return_value = Mock(content="") + + def test_block_attack_gets_client(self): + block_attack(["BlockSqliBruteForce"]) + + self.get_client.assert_called_once_with() + + def test_block_attack_blocks_attack(self): + block_attack(["BlockSqliBruteForce"]) + + self.client.block_attack.assert_called_once_with( + "myappname", "BlockSqliBruteForce" + ) + + def test_block_attack_prints_output_on_success(self): + block_attack(["BlockSqliBruteForce"]) + + self.print.assert_called_once_with( + "A job to block the 'BlockSqliBruteForce' attack has been posted." + ) + + def test_block_attack_prints_output_on_failure(self): + self.client.block_attack.return_value = Mock( + content='{"attack_name":["\\"BlockDownloaderBruteForce\\" is not a valid choice."]}' + ) + + block_attack(["BlockDownloaderBruteForce"]) + + self.print.assert_called_once_with( + '{"attack_name":["\\"BlockDownloaderBruteForce\\" is not a valid choice."]}' + ) + + def test_block_attack_raises_on_invalid_choice(self): + with self.assertRaises(SystemExit): + # We get the valid choices from get_block_attack_descriptions + block_attack(["DoesNotExist"]) diff --git a/tests/commands/test_get_app_configurations.py b/tests/commands/test_get_app_configurations.py new file mode 100644 index 0000000..e0dff12 --- /dev/null +++ b/tests/commands/test_get_app_configurations.py @@ -0,0 +1,28 @@ +from hypernode_api_python.commands import get_app_configurations +from tests.testcase import TestCase + + +class TestGetAppConfigurations(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + + def test_get_app_configurations_gets_client(self): + get_app_configurations([]) + + self.get_client.assert_called_once_with() + + def test_get_app_configurations_gets_slas(self): + get_app_configurations([]) + + self.client.get_app_configurations.assert_called_once_with() + + def test_get_app_configurations_prints_slas(self): + get_app_configurations([]) + + self.print_response.assert_called_once_with( + self.client.get_app_configurations.return_value + ) diff --git a/tests/commands/test_get_app_flavor.py b/tests/commands/test_get_app_flavor.py new file mode 100644 index 0000000..9493448 --- /dev/null +++ b/tests/commands/test_get_app_flavor.py @@ -0,0 +1,37 @@ +from hypernode_api_python.commands import get_app_flavor +from tests.testcase import TestCase + + +class TestGetAppFlavor(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_get_app_flavor_gets_client(self): + get_app_flavor([]) + + self.get_client.assert_called_once_with() + + def test_get_app_flavor_gets_app_name(self): + get_app_flavor([]) + + self.get_app_name.assert_called_once_with() + + def test_get_app_flavor_gets_app_flavor(self): + get_app_flavor([]) + + self.client.get_app_flavor.assert_called_once_with("myappname") + + def test_get_app_flavor_prints_app_flavor(self): + get_app_flavor([]) + + self.print_response.assert_called_once_with( + self.client.get_app_flavor.return_value + ) diff --git a/tests/commands/test_get_app_info.py b/tests/commands/test_get_app_info.py new file mode 100644 index 0000000..e83ae99 --- /dev/null +++ b/tests/commands/test_get_app_info.py @@ -0,0 +1,37 @@ +from hypernode_api_python.commands import get_app_info +from tests.testcase import TestCase + + +class TestGetAppInfo(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_get_app_info_gets_client(self): + get_app_info([]) + + self.get_client.assert_called_once_with() + + def test_get_app_info_gets_app_name(self): + get_app_info([]) + + self.get_app_name.assert_called_once_with() + + def test_get_app_info_gets_app_info(self): + get_app_info([]) + + self.client.get_app_info_or_404.assert_called_once_with("myappname") + + def test_get_app_info_prints_app_info(self): + get_app_info([]) + + self.print_response.assert_called_once_with( + self.client.get_app_info_or_404.return_value + ) diff --git a/tests/commands/test_get_app_name.py b/tests/commands/test_get_app_name.py new file mode 100644 index 0000000..3b2321a --- /dev/null +++ b/tests/commands/test_get_app_name.py @@ -0,0 +1,29 @@ +from hypernode_api_python.commands import get_app_name +from tests.testcase import TestCase + + +class TestGetAppName(TestCase): + def setUp(self): + self.expected_environment = { + "HYPERNODE_APP_NAME": "myappname", + } + self.set_up_patch( + "hypernode_api_python.commands.environ", self.expected_environment + ) + + def test_get_app_name_returns_app_name(self): + ret = get_app_name() + + self.assertEqual(ret, "myappname") + + def test_get_app_name_raises_value_error_if_no_app_name(self): + del self.expected_environment["HYPERNODE_APP_NAME"] + + with self.assertRaises(ValueError): + get_app_name() + + def test_get_app_name_raises_value_error_if_empty_app_name(self): + self.expected_environment["HYPERNODE_APP_NAME"] = "" + + with self.assertRaises(ValueError): + get_app_name() diff --git a/tests/commands/test_get_available_backups_for_app.py b/tests/commands/test_get_available_backups_for_app.py new file mode 100644 index 0000000..3a8d0e8 --- /dev/null +++ b/tests/commands/test_get_available_backups_for_app.py @@ -0,0 +1,37 @@ +from hypernode_api_python.commands import get_available_backups_for_app +from tests.testcase import TestCase + + +class TestGetAvailableBackupsForApp(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_get_available_backups_for_app_gets_client(self): + get_available_backups_for_app([]) + + self.get_client.assert_called_once_with() + + def test_get_available_backups_for_app_gets_app_name(self): + get_available_backups_for_app([]) + + self.get_app_name.assert_called_once_with() + + def test_get_available_backups_for_app_gets_available_backups_for_app(self): + get_available_backups_for_app([]) + + self.client.get_available_backups_for_app.assert_called_once_with("myappname") + + def test_get_available_backups_for_app_prints_available_backups_for_app(self): + get_available_backups_for_app([]) + + self.print_response.assert_called_once_with( + self.client.get_available_backups_for_app.return_value + ) diff --git a/tests/commands/test_get_block_attack_descriptions.py b/tests/commands/test_get_block_attack_descriptions.py new file mode 100644 index 0000000..e9a161c --- /dev/null +++ b/tests/commands/test_get_block_attack_descriptions.py @@ -0,0 +1,28 @@ +from hypernode_api_python.commands import get_block_attack_descriptions +from tests.testcase import TestCase + + +class TestGetBlockAttackDescriptions(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + + def test_get_block_attack_descriptions_gets_client(self): + get_block_attack_descriptions([]) + + self.get_client.assert_called_once_with() + + def test_get_block_attack_descriptions_gets_slas(self): + get_block_attack_descriptions([]) + + self.client.get_block_attack_descriptions.assert_called_once_with() + + def test_get_block_attack_descriptions_prints_slas(self): + get_block_attack_descriptions([]) + + self.print_response.assert_called_once_with( + self.client.get_block_attack_descriptions.return_value + ) diff --git a/tests/commands/test_get_client.py b/tests/commands/test_get_client.py new file mode 100644 index 0000000..81e4d51 --- /dev/null +++ b/tests/commands/test_get_client.py @@ -0,0 +1,37 @@ +from hypernode_api_python.commands import get_client +from tests.testcase import TestCase + + +class TestGetClient(TestCase): + def setUp(self): + self.hypernode_api_python = self.set_up_patch( + "hypernode_api_python.commands.HypernodeAPIPython" + ) + self.expected_environment = { + "HYPERNODE_API_TOKEN": "mytoken", + } + self.set_up_patch( + "hypernode_api_python.commands.environ", self.expected_environment + ) + + def test_get_client_instantiates_client(self): + get_client() + + self.hypernode_api_python.assert_called_once_with("mytoken") + + def test_get_client_returns_instantiated_client(self): + ret = get_client() + + self.assertEqual(ret, self.hypernode_api_python.return_value) + + def test_get_client_raises_value_error_if_no_token(self): + del self.expected_environment["HYPERNODE_API_TOKEN"] + + with self.assertRaises(ValueError): + get_client() + + def test_get_client_raises_value_error_if_empty_token(self): + self.expected_environment["HYPERNODE_API_TOKEN"] = "" + + with self.assertRaises(ValueError): + get_client() diff --git a/tests/commands/test_get_cluster_relations.py b/tests/commands/test_get_cluster_relations.py new file mode 100644 index 0000000..c77cb00 --- /dev/null +++ b/tests/commands/test_get_cluster_relations.py @@ -0,0 +1,37 @@ +from hypernode_api_python.commands import get_cluster_relations +from tests.testcase import TestCase + + +class TestGetClusterRelations(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_get_cluster_relations_gets_client(self): + get_cluster_relations([]) + + self.get_client.assert_called_once_with() + + def test_get_cluster_relations_gets_app_name(self): + get_cluster_relations([]) + + self.get_app_name.assert_called_once_with() + + def test_get_cluster_relations_gets_cluster_relations(self): + get_cluster_relations([]) + + self.client.get_cluster_relations.assert_called_once_with("myappname") + + def test_get_cluster_relations_prints_cluster_relations(self): + get_cluster_relations([]) + + self.print_response.assert_called_once_with( + self.client.get_cluster_relations.return_value + ) diff --git a/tests/commands/test_get_eav_description.py b/tests/commands/test_get_eav_description.py new file mode 100644 index 0000000..b73fa63 --- /dev/null +++ b/tests/commands/test_get_eav_description.py @@ -0,0 +1,28 @@ +from hypernode_api_python.commands import get_eav_description +from tests.testcase import TestCase + + +class TestGetEavDescription(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + + def test_get_eav_description_gets_client(self): + get_eav_description([]) + + self.get_client.assert_called_once_with() + + def test_get_eav_description_gets_slas(self): + get_eav_description([]) + + self.client.get_app_eav_description.assert_called_once_with() + + def test_get_eav_description_prints_slas(self): + get_eav_description([]) + + self.print_response.assert_called_once_with( + self.client.get_app_eav_description.return_value + ) diff --git a/tests/commands/test_get_flows.py b/tests/commands/test_get_flows.py new file mode 100644 index 0000000..ed01b13 --- /dev/null +++ b/tests/commands/test_get_flows.py @@ -0,0 +1,35 @@ +from hypernode_api_python.commands import get_flows +from tests.testcase import TestCase + + +class TestGetFlows(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_get_flows_gets_client(self): + get_flows([]) + + self.get_client.assert_called_once_with() + + def test_get_flows_gets_app_name(self): + get_flows([]) + + self.get_app_name.assert_called_once_with() + + def test_get_flows_gets_flows(self): + get_flows([]) + + self.client.get_flows.assert_called_once_with("myappname") + + def test_get_flows_prints_flows(self): + get_flows([]) + + self.print_response.assert_called_once_with(self.client.get_flows.return_value) diff --git a/tests/commands/test_get_next_best_plan.py b/tests/commands/test_get_next_best_plan.py new file mode 100644 index 0000000..75809c5 --- /dev/null +++ b/tests/commands/test_get_next_best_plan.py @@ -0,0 +1,39 @@ +from hypernode_api_python.commands import get_next_best_plan_for_app +from tests.testcase import TestCase + + +class TestGetNextBestPlan(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_get_next_best_plan_for_app_gets_client(self): + get_next_best_plan_for_app([]) + + self.get_client.assert_called_once_with() + + def test_get_next_best_plan_for_app_gets_app_name(self): + get_next_best_plan_for_app([]) + + self.get_app_name.assert_called_once_with() + + def test_get_next_best_plan_for_app_gets_next_best_plan_for_app(self): + get_next_best_plan_for_app([]) + + self.client.get_next_best_plan_for_app_or_404.assert_called_once_with( + "myappname" + ) + + def test_get_next_best_plan_for_app_prints_next_best_plan_for_app(self): + get_next_best_plan_for_app([]) + + self.print_response.assert_called_once_with( + self.client.get_next_best_plan_for_app_or_404.return_value + ) diff --git a/tests/commands/test_get_product_info.py b/tests/commands/test_get_product_info.py new file mode 100644 index 0000000..890c16f --- /dev/null +++ b/tests/commands/test_get_product_info.py @@ -0,0 +1,30 @@ +from hypernode_api_python.commands import get_product_info +from tests.testcase import TestCase + + +class TestGetProductInfo(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + + def test_get_product_info_gets_client(self): + get_product_info(["FALCON_S_202203"]) + + self.get_client.assert_called_once_with() + + def test_get_product_info_gets_product_info_with_price(self): + get_product_info(["FALCON_S_202203"]) + + self.client.get_product_info_with_price.assert_called_once_with( + "FALCON_S_202203" + ) + + def test_get_product_info_prints_product(self): + get_product_info(["FALCON_S_202203"]) + + self.print_response.assert_called_once_with( + self.client.get_product_info_with_price.return_value + ) diff --git a/tests/commands/test_get_sla.py b/tests/commands/test_get_sla.py new file mode 100644 index 0000000..abbb026 --- /dev/null +++ b/tests/commands/test_get_sla.py @@ -0,0 +1,26 @@ +from hypernode_api_python.commands import get_sla +from tests.testcase import TestCase + + +class TestGetSla(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + + def test_get_sla_gets_client(self): + get_sla(["sla-standard"]) + + self.get_client.assert_called_once_with() + + def test_get_sla_gets_sla(self): + get_sla(["sla-standard"]) + + self.client.get_sla.assert_called_once_with("sla-standard") + + def test_get_sla_prints_sla(self): + get_sla(["sla-standard"]) + + self.print_response.assert_called_once_with(self.client.get_sla.return_value) diff --git a/tests/commands/test_get_slas.py b/tests/commands/test_get_slas.py new file mode 100644 index 0000000..d545f13 --- /dev/null +++ b/tests/commands/test_get_slas.py @@ -0,0 +1,26 @@ +from hypernode_api_python.commands import get_slas +from tests.testcase import TestCase + + +class TestGetSlas(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + + def test_get_slas_gets_client(self): + get_slas([]) + + self.get_client.assert_called_once_with() + + def test_get_slas_gets_slas(self): + get_slas([]) + + self.client.get_slas.assert_called_once_with() + + def test_get_slas_prints_slas(self): + get_slas([]) + + self.print_response.assert_called_once_with(self.client.get_slas.return_value) diff --git a/tests/commands/test_get_whitelist_options.py b/tests/commands/test_get_whitelist_options.py new file mode 100644 index 0000000..6b989d6 --- /dev/null +++ b/tests/commands/test_get_whitelist_options.py @@ -0,0 +1,37 @@ +from hypernode_api_python.commands import get_whitelist_options +from tests.testcase import TestCase + + +class TestGetWhitelistOptions(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_get_whitelist_options_gets_client(self): + get_whitelist_options([]) + + self.get_client.assert_called_once_with() + + def test_get_whitelist_options_gets_app_name(self): + get_whitelist_options([]) + + self.get_app_name.assert_called_once_with() + + def test_get_whitelist_options_gets_whitelist_options(self): + get_whitelist_options([]) + + self.client.get_whitelist_options.assert_called_once_with("myappname") + + def test_get_whitelist_options_prints_whitelist_options(self): + get_whitelist_options([]) + + self.print_response.assert_called_once_with( + self.client.get_whitelist_options.return_value + ) diff --git a/tests/commands/test_get_whitelist_rules.py b/tests/commands/test_get_whitelist_rules.py new file mode 100644 index 0000000..daca133 --- /dev/null +++ b/tests/commands/test_get_whitelist_rules.py @@ -0,0 +1,37 @@ +from hypernode_api_python.commands import get_whitelist_rules +from tests.testcase import TestCase + + +class TestGetWhitelistRules(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_get_whitelist_rules_gets_client(self): + get_whitelist_rules([]) + + self.get_client.assert_called_once_with() + + def test_get_whitelist_rules_gets_app_name(self): + get_whitelist_rules([]) + + self.get_app_name.assert_called_once_with() + + def test_get_whitelist_rules_gets_whitelist_rules(self): + get_whitelist_rules([]) + + self.client.get_whitelist_rules.assert_called_once_with("myappname") + + def test_get_whitelist_rules_prints_whitelist_rules(self): + get_whitelist_rules([]) + + self.print_response.assert_called_once_with( + self.client.get_whitelist_rules.return_value + ) diff --git a/tests/commands/test_print_response.py b/tests/commands/test_print_response.py new file mode 100644 index 0000000..d72a606 --- /dev/null +++ b/tests/commands/test_print_response.py @@ -0,0 +1,32 @@ +from unittest.mock import Mock + +from hypernode_api_python.commands import print_response +from tests.testcase import TestCase + + +class TestPrintResponse(TestCase): + def setUp(self): + self.response = Mock() + self.response.json.return_value = { + "id": 123, + "code": "sla-standard", + "name": "SLA Standard", + "price": 1234, + "billing_period": 1, + "billing_period_unit": "month", + } + + self.print = self.set_up_patch("hypernode_api_python.commands.print") + + def test_print_response_prints_pretty_response(self): + print_response(self.response) + + self.print.assert_called_once_with( + '{\n "id": 123,\n "code": "sla-standard",\n "name": "SLA Standard",\n ' + '"price": 1234,\n "billing_period": 1,\n "billing_period_unit": "month"\n}' + ) + + def test_print_response_returns_none(self): + ret = print_response(self.response) + + self.assertIsNone(ret) diff --git a/tests/commands/test_validate_app_name.py b/tests/commands/test_validate_app_name.py new file mode 100644 index 0000000..b0c3b73 --- /dev/null +++ b/tests/commands/test_validate_app_name.py @@ -0,0 +1,55 @@ +from os import EX_UNAVAILABLE, EX_OK + +from hypernode_api_python.commands import validate_app_name +from tests.testcase import TestCase + + +class TestValidateAppName(TestCase): + def setUp(self): + self.exit = self.set_up_patch("hypernode_api_python.commands.exit") + self.print = self.set_up_patch("hypernode_api_python.commands.print") + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + + def test_validate_app_name_gets_client(self): + validate_app_name(["myappname"]) + + self.get_client.assert_called_once_with() + + def test_validate_app_name_validates_app_name(self): + validate_app_name(["myappname"]) + + self.client.validate_app_name.assert_called_once_with("myappname") + + def test_validate_app_name_prints_app_name_is_valid(self): + validate_app_name(["myappname"]) + + self.print.assert_called_once_with("App name 'myappname' is valid.") + + def test_validate_app_name_prints_app_name_is_invalid(self): + self.client.validate_app_name.side_effect = RuntimeError( + "[\"This value can only contain non-capital letters 'a' through 'z' or digits " + '0 through 9."]' + ) + + validate_app_name(["myappname"]) + + self.print.assert_called_once_with( + "App name 'myappname' is invalid: [\"This value can only contain non-capital letters " + "'a' through 'z' or digits 0 through 9.\"]" + ) + + def test_validate_app_name_exits_zero_when_valid(self): + validate_app_name(["myappname"]) + + self.exit.assert_called_once_with(EX_OK) + + def test_validate_app_name_exits_nonzero_when_invalid(self): + self.client.validate_app_name.side_effect = RuntimeError + + validate_app_name(["myappname"]) + + self.exit.assert_called_once_with(EX_UNAVAILABLE) diff --git a/tox.ini b/tox.ini index a596b98..54ee538 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{3.7,3.8,3.9} +envlist = py{3.7,3.8,3.9,3.10,3.11} skipsdist = True skip_missing_interpreters = True @@ -9,6 +9,8 @@ basepython = py3.7: python3.7 py3.8: python3.8 py3.9: python3.9 + py3.10: python3.10 + py3.11: python3.11 deps = -rrequirements/development.txt From 67c8077cd484780b44ff607c94d562a90c95dbc5 Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Sat, 25 May 2024 15:58:14 +0200 Subject: [PATCH 31/38] implement bin/get_current_product_for_app --- bin/get_current_product_for_app | 1 + hypernode_api_python/client.py | 1 - hypernode_api_python/commands.py | 31 ++++++++++++++++ .../test_get_current_product_for_app.py | 37 +++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 120000 bin/get_current_product_for_app create mode 100644 tests/commands/test_get_current_product_for_app.py diff --git a/bin/get_current_product_for_app b/bin/get_current_product_for_app new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_current_product_for_app @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index b757fcb..f391d16 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -580,7 +580,6 @@ def get_whitelist_rules(self, app_name, filter_data=None): "GET", HYPERNODE_API_WHITELIST_ENDPOINT.format(app_name), filter_data ) - # TODO: add entrypoint for this method in bin/ and commands.py def get_current_product_for_app(self, app_name): """ Retrieve information about the product the specified App is currently on. diff --git a/hypernode_api_python/commands.py b/hypernode_api_python/commands.py index c945fe4..f0d5a0c 100644 --- a/hypernode_api_python/commands.py +++ b/hypernode_api_python/commands.py @@ -477,3 +477,34 @@ def get_whitelist_rules(args=None): client = get_client() app_name = get_app_name() print_response(client.get_whitelist_rules(app_name)) + + +def get_current_product_for_app(args=None): + parser = ArgumentParser( + description=""" +Gets the current product for the specified app. + +Example: +$ ./bin/get_current_product_for_app +{ + "code": "FALCON_M_202203", + "name": "Falcon M", + "backups_enabled": true, + "is_development": false, + "varnish_supported": true, + "supports_sla": true, + "provider_flavors": [ + { + "vcpus": 3, + "ram_in_mb": 16384, + ... + }, + ... +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.get_current_product_for_app(app_name)) diff --git a/tests/commands/test_get_current_product_for_app.py b/tests/commands/test_get_current_product_for_app.py new file mode 100644 index 0000000..e19d9d6 --- /dev/null +++ b/tests/commands/test_get_current_product_for_app.py @@ -0,0 +1,37 @@ +from hypernode_api_python.commands import get_current_product_for_app +from tests.testcase import TestCase + + +class TestGetCurrentProductForApp(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_get_current_product_for_app_gets_client(self): + get_current_product_for_app([]) + + self.get_client.assert_called_once_with() + + def test_get_current_product_for_app_gets_app_name(self): + get_current_product_for_app([]) + + self.get_app_name.assert_called_once_with() + + def test_get_current_product_for_app_gets_current_product_for_app(self): + get_current_product_for_app([]) + + self.client.get_current_product_for_app.assert_called_once_with("myappname") + + def test_get_current_product_for_app_prints_current_product_for_app(self): + get_current_product_for_app([]) + + self.print_response.assert_called_once_with( + self.client.get_current_product_for_app.return_value + ) From b24b26f440b45895465a0ef3a4915f661f30f9aa Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Sat, 25 May 2024 19:52:26 +0200 Subject: [PATCH 32/38] entrypoint check_payment_info / get_active_product --- bin/check_payment_information_for_app | 1 + bin/get_active_products | 1 + hypernode_api_python/client.py | 2 - hypernode_api_python/commands.py | 45 +++++++++++++++++++ .../test_check_payment_information_for_app.py | 39 ++++++++++++++++ tests/commands/test_get_active_products.py | 28 ++++++++++++ 6 files changed, 114 insertions(+), 2 deletions(-) create mode 120000 bin/check_payment_information_for_app create mode 120000 bin/get_active_products create mode 100644 tests/commands/test_check_payment_information_for_app.py create mode 100644 tests/commands/test_get_active_products.py diff --git a/bin/check_payment_information_for_app b/bin/check_payment_information_for_app new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/check_payment_information_for_app @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/get_active_products b/bin/get_active_products new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_active_products @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index f391d16..642cbbe 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -648,7 +648,6 @@ def get_current_product_for_app(self, app_name): "GET", HYPERNODE_API_PRODUCT_APP_DETAIL_ENDPOINT.format(app_name) ) - # TODO: add entrypoint for this method in bin/ and commands.py def check_payment_information_for_app(self, app_name): """ Get the payment information that is currently configured for this Hypernode @@ -666,7 +665,6 @@ def check_payment_information_for_app(self, app_name): "GET", HYPERNODE_API_APP_CHECK_PAYMENT_INFORMATION.format(app_name) ) - # TODO: add entrypoint for this method in bin/ and commands.py def get_active_products(self): """ Retrieve the list of products that are currently available. You can diff --git a/hypernode_api_python/commands.py b/hypernode_api_python/commands.py index f0d5a0c..e67cd8a 100644 --- a/hypernode_api_python/commands.py +++ b/hypernode_api_python/commands.py @@ -508,3 +508,48 @@ def get_current_product_for_app(args=None): client = get_client() app_name = get_app_name() print_response(client.get_current_product_for_app(app_name)) + + +def check_payment_information_for_app(args=None): + parser = ArgumentParser( + description=""" +Shows the payment information for the specified app. + +Example: +$ ./bin/check_payment_information_for_app +{ + "has_valid_vat_number": true, + "has_valid_payment_method": true +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.check_payment_information_for_app(app_name)) + + +def get_active_products(args=None): + parser = ArgumentParser( + description=""" +Lists all available products. + +Example: +$ ./bin/get_active_products +[ + { + "code": "JACKAL_S_202301", + "name": "Jackal S", + "backups_enabled": true, + "is_development": false, + ... + }, + ... +] +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + print_response(client.get_active_products()) diff --git a/tests/commands/test_check_payment_information_for_app.py b/tests/commands/test_check_payment_information_for_app.py new file mode 100644 index 0000000..114cb39 --- /dev/null +++ b/tests/commands/test_check_payment_information_for_app.py @@ -0,0 +1,39 @@ +from hypernode_api_python.commands import check_payment_information_for_app +from tests.testcase import TestCase + + +class TestCheckPaymentInformationForApp(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_check_payment_information_for_app_gets_client(self): + check_payment_information_for_app([]) + + self.get_client.assert_called_once_with() + + def test_check_payment_information_for_app_gets_app_name(self): + check_payment_information_for_app([]) + + self.get_app_name.assert_called_once_with() + + def test_check_payment_information_for_app_gets_current_product_for_app(self): + check_payment_information_for_app([]) + + self.client.check_payment_information_for_app.assert_called_once_with( + "myappname" + ) + + def test_check_payment_information_for_app_prints_current_product_for_app(self): + check_payment_information_for_app([]) + + self.print_response.assert_called_once_with( + self.client.check_payment_information_for_app.return_value + ) diff --git a/tests/commands/test_get_active_products.py b/tests/commands/test_get_active_products.py new file mode 100644 index 0000000..284fb39 --- /dev/null +++ b/tests/commands/test_get_active_products.py @@ -0,0 +1,28 @@ +from hypernode_api_python.commands import get_active_products +from tests.testcase import TestCase + + +class TestGetActiveProducts(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + + def test_get_active_products_gets_client(self): + get_active_products([]) + + self.get_client.assert_called_once_with() + + def test_get_active_products_gets_active_products(self): + get_active_products([]) + + self.client.get_active_products.assert_called_once_with() + + def test_get_active_products_prints_active_products(self): + get_active_products([]) + + self.print_response.assert_called_once_with( + self.client.get_active_products.return_value + ) From d2150742f60330caad81e357cb81b147fd3fbc97 Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Sat, 25 May 2024 20:00:44 +0200 Subject: [PATCH 33/38] implement bin/check_xgrade --- bin/check_xgrade | 1 + hypernode_api_python/commands.py | 27 +++++++++++++++++++++ tests/commands/test_check_xgrade.py | 37 +++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 120000 bin/check_xgrade create mode 100644 tests/commands/test_check_xgrade.py diff --git a/bin/check_xgrade b/bin/check_xgrade new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/check_xgrade @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/hypernode_api_python/commands.py b/hypernode_api_python/commands.py index e67cd8a..1f167a9 100644 --- a/hypernode_api_python/commands.py +++ b/hypernode_api_python/commands.py @@ -553,3 +553,30 @@ def get_active_products(args=None): parser.parse_args(args=args) client = get_client() print_response(client.get_active_products()) + + +def check_xgrade(args=None): + parser = ArgumentParser( + description=""" +Verify that the specified app can be upgraded to the specified product. +This checks if there is enough disk space available. The output will also +show whether or not there will be an IP change and if a volume swap xgrade +would be performed instead of an rsync xgrade. + +Example: +$ ./bin/check_xgrade FALCON_L_202203 +{ + "has_valid_vat_number": true, + "has_valid_payment_method": true, + "will_change_ip": false, + "will_do_volswap": false, + "will_disk_fit": true +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.add_argument("product_code", help="The code of the product to check") + args = parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.check_xgrade(app_name, args.product_code)) diff --git a/tests/commands/test_check_xgrade.py b/tests/commands/test_check_xgrade.py new file mode 100644 index 0000000..acc1953 --- /dev/null +++ b/tests/commands/test_check_xgrade.py @@ -0,0 +1,37 @@ +from hypernode_api_python.commands import check_xgrade +from tests.testcase import TestCase + + +class TestCheckXgrade(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_check_xgrade_gets_client(self): + check_xgrade(["FALCON_S_202203"]) + + self.get_client.assert_called_once_with() + + def test_check_xgrade_gets_app_name(self): + check_xgrade(["FALCON_S_202203"]) + + self.get_app_name.assert_called_once_with() + + def test_check_xgrade_gets_product_info(self): + check_xgrade(["FALCON_S_202203"]) + + self.client.check_xgrade.assert_called_once_with("myappname", "FALCON_S_202203") + + def test_check_xgrade_prints_product(self): + check_xgrade(["FALCON_S_202203"]) + + self.print_response.assert_called_once_with( + self.client.check_xgrade.return_value + ) From 26de3c8491d89c29e96e5dcaa5aed6fc2293e0cc Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Sat, 25 May 2024 20:07:54 +0200 Subject: [PATCH 34/38] add choices for get_prod_info and check_xgrade --- hypernode_api_python/commands.py | 16 ++++++++++++---- tests/commands/test_check_xgrade.py | 12 ++++++++++++ tests/commands/test_get_product_info.py | 12 ++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/hypernode_api_python/commands.py b/hypernode_api_python/commands.py index 1f167a9..032e6ee 100644 --- a/hypernode_api_python/commands.py +++ b/hypernode_api_python/commands.py @@ -379,9 +379,13 @@ def get_product_info(args=None): """, formatter_class=RawTextHelpFormatter, ) - parser.add_argument("product_code", help="The code of the product to get") - args = parser.parse_args(args=args) client = get_client() + parser.add_argument( + "product_code", + help="The code of the product to get", + choices=[p["code"] for p in client.get_active_products().json()], + ) + args = parser.parse_args(args=args) print_response(client.get_product_info_with_price(args.product_code)) @@ -575,8 +579,12 @@ def check_xgrade(args=None): """, formatter_class=RawTextHelpFormatter, ) - parser.add_argument("product_code", help="The code of the product to check") - args = parser.parse_args(args=args) client = get_client() + parser.add_argument( + "product_code", + help="The code of the product to check", + choices=[p["code"] for p in client.get_active_products().json()], + ) + args = parser.parse_args(args=args) app_name = get_app_name() print_response(client.check_xgrade(app_name, args.product_code)) diff --git a/tests/commands/test_check_xgrade.py b/tests/commands/test_check_xgrade.py index acc1953..8ee612e 100644 --- a/tests/commands/test_check_xgrade.py +++ b/tests/commands/test_check_xgrade.py @@ -13,6 +13,14 @@ def setUp(self): "hypernode_api_python.commands.get_app_name" ) self.get_app_name.return_value = "myappname" + self.client.get_active_products.return_value.json.return_value = [ + { + "code": "FALCON_S_202203", + }, + { + "code": "JACKAL_M_202201", + }, + ] def test_check_xgrade_gets_client(self): check_xgrade(["FALCON_S_202203"]) @@ -35,3 +43,7 @@ def test_check_xgrade_prints_product(self): self.print_response.assert_called_once_with( self.client.check_xgrade.return_value ) + + def test_check_xgrade_raises_if_product_not_found(self): + with self.assertRaises(SystemExit): + check_xgrade(["ProductThatDoesNotExist"]) diff --git a/tests/commands/test_get_product_info.py b/tests/commands/test_get_product_info.py index 890c16f..e3a6156 100644 --- a/tests/commands/test_get_product_info.py +++ b/tests/commands/test_get_product_info.py @@ -9,6 +9,14 @@ def setUp(self): ) self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") self.client = self.get_client.return_value + self.client.get_active_products.return_value.json.return_value = [ + { + "code": "FALCON_S_202203", + }, + { + "code": "JACKAL_M_202201", + }, + ] def test_get_product_info_gets_client(self): get_product_info(["FALCON_S_202203"]) @@ -28,3 +36,7 @@ def test_get_product_info_prints_product(self): self.print_response.assert_called_once_with( self.client.get_product_info_with_price.return_value ) + + def test_get_product_raises_if_product_not_found(self): + with self.assertRaises(SystemExit): + get_product_info(["ProductThatDoesNotExist"]) From 43198c77c7789157a850df920de66b27484348c8 Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Sat, 25 May 2024 20:17:52 +0200 Subject: [PATCH 35/38] implement bin/xgrade --- bin/xgrade | 1 + hypernode_api_python/client.py | 1 - hypernode_api_python/commands.py | 30 ++++++++++++++++ tests/commands/test_xgrade.py | 62 ++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 120000 bin/xgrade create mode 100644 tests/commands/test_xgrade.py diff --git a/bin/xgrade b/bin/xgrade new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/xgrade @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 642cbbe..bcd3f1e 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -761,7 +761,6 @@ def check_xgrade(self, app_name, product_code): HYPERNODE_API_APP_XGRADE_CHECK_ENDPOINT.format(app_name, product_code), ) - # TODO: add entrypoint for this method in bin/ and commands.py def xgrade(self, app_name, data): """ Change the product of a Hypernode to a different plan. This will initiate diff --git a/hypernode_api_python/commands.py b/hypernode_api_python/commands.py index 032e6ee..2817e48 100644 --- a/hypernode_api_python/commands.py +++ b/hypernode_api_python/commands.py @@ -588,3 +588,33 @@ def check_xgrade(args=None): args = parser.parse_args(args=args) app_name = get_app_name() print_response(client.check_xgrade(app_name, args.product_code)) + + +def xgrade(args=None): + parser = ArgumentParser( + description=""" +Change the plan of your Hypernode. + +Example: +$ ./bin/xgrade FALCON_L_202203 +The job to xgrade Hypernode 'yourappname' to product 'FALCON_L_202203' has been posted +""", + formatter_class=RawTextHelpFormatter, + ) + client = get_client() + parser.add_argument( + "product_code", + help="The code of the product to check", + choices=[p["code"] for p in client.get_active_products().json()], + ) + args = parser.parse_args(args=args) + app_name = get_app_name() + data = {"product": args.product_code} + output = client.xgrade(app_name, data=data).content + if output: + print(output) + else: + print( + "The job to xgrade Hypernode '{}' to product '{}' " + "has been posted".format(app_name, args.product_code) + ) diff --git a/tests/commands/test_xgrade.py b/tests/commands/test_xgrade.py new file mode 100644 index 0000000..cad88ce --- /dev/null +++ b/tests/commands/test_xgrade.py @@ -0,0 +1,62 @@ +from unittest.mock import Mock + +from hypernode_api_python.commands import xgrade +from tests.testcase import TestCase + + +class TestXgrade(TestCase): + def setUp(self): + self.print = self.set_up_patch("hypernode_api_python.commands.print") + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + self.client.get_xgrade_descriptions.return_value = Mock( + json=lambda: {"FALCON_L_202203": "", "BlockDownloaderBruteForce": ""} + ) + self.client.xgrade.return_value = Mock(content="") + self.client.get_active_products.return_value.json.return_value = [ + { + "code": "FALCON_L_202203", + }, + { + "code": "FALCON_6XL_202203", + }, + ] + + def test_xgrade_gets_client(self): + xgrade(["FALCON_L_202203"]) + + self.get_client.assert_called_once_with() + + def test_xgrade_performs_xgrade(self): + xgrade(["FALCON_L_202203"]) + + self.client.xgrade.assert_called_once_with( + "myappname", data={"product": "FALCON_L_202203"} + ) + + def test_xgrade_prints_output_on_success(self): + xgrade(["FALCON_L_202203"]) + + self.print.assert_called_once_with( + "The job to xgrade Hypernode 'myappname' to product 'FALCON_L_202203' has been posted" + ) + + def test_xgrade_prints_output_on_failure(self): + self.client.xgrade.return_value = Mock( + content='{"product":["Object with code=FALCON_6XL_202203 does not exist."]}' + ) + + xgrade(["FALCON_6XL_202203"]) + + self.print.assert_called_once_with( + '{"product":["Object with code=FALCON_6XL_202203 does not exist."]}' + ) + + def test_xgrade_raises_on_invalid_choice(self): + with self.assertRaises(SystemExit): + # We get the valid choices from get_xgrade_descriptions + xgrade(["DoesNotExist"]) From 67873e684ed1ac1ff3e2e2da4e2a3c4b07698e23 Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Sat, 25 May 2024 20:22:56 +0200 Subject: [PATCH 36/38] add ./bin/get_active_branchers --- bin/get_active_branchers | 1 + hypernode_api_python/client.py | 2 -- hypernode_api_python/commands.py | 23 +++++++++++++ tests/commands/test_get_active_branchers.py | 37 +++++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) create mode 120000 bin/get_active_branchers create mode 100644 tests/commands/test_get_active_branchers.py diff --git a/bin/get_active_branchers b/bin/get_active_branchers new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_active_branchers @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index bcd3f1e..1eb214f 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -732,7 +732,6 @@ def get_active_products(self): """ return self.requests("GET", HYPERNODE_API_PRODUCT_LIST_ENDPOINT) - # TODO: add entrypoint for this method in bin/ and commands.py def check_xgrade(self, app_name, product_code): """ Checks if the Hypernode 'is going to fit' on the new product. Retrieves some @@ -802,7 +801,6 @@ def order_hypernode(self, data): """ return self.requests("POST", HYPERNODE_API_APP_ORDER_ENDPOINT, data=data) - # TODO: add entrypoint for this method in bin/ and commands.py def get_active_branchers(self, app_name): """ List all active brancher nodes of your Hypernode. diff --git a/hypernode_api_python/commands.py b/hypernode_api_python/commands.py index 2817e48..131b87a 100644 --- a/hypernode_api_python/commands.py +++ b/hypernode_api_python/commands.py @@ -618,3 +618,26 @@ def xgrade(args=None): "The job to xgrade Hypernode '{}' to product '{}' " "has been posted".format(app_name, args.product_code) ) + + +def get_active_branchers(args=None): + parser = ArgumentParser( + description=""" +List all active branchers + +Example: +$ ./bin/get_active_branchers +{ + "monthly_total_time": 0, + "total_minutes_elapsed": 0, + "actual_monthly_total_cost": 0, + "monthly_total_cost": 0, + "branchers": [] +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.get_active_branchers(app_name)) diff --git a/tests/commands/test_get_active_branchers.py b/tests/commands/test_get_active_branchers.py new file mode 100644 index 0000000..c4b2316 --- /dev/null +++ b/tests/commands/test_get_active_branchers.py @@ -0,0 +1,37 @@ +from hypernode_api_python.commands import get_active_branchers +from tests.testcase import TestCase + + +class TestGetActiveBranchers(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_get_active_branchers_gets_client(self): + get_active_branchers([]) + + self.get_client.assert_called_once_with() + + def test_get_active_branchers_gets_app_name(self): + get_active_branchers([]) + + self.get_app_name.assert_called_once_with() + + def test_get_active_branchers_gets_active_branchers(self): + get_active_branchers([]) + + self.client.get_active_branchers.assert_called_once_with("myappname") + + def test_get_active_branchers_prints_active_branchers(self): + get_active_branchers([]) + + self.print_response.assert_called_once_with( + self.client.get_active_branchers.return_value + ) From d0796783ad30dd24ceeeae967e4b9dee7222e015 Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Sun, 26 May 2024 12:08:50 +0200 Subject: [PATCH 37/38] add bin/create_brancher and bin/destroy_brancher --- bin/create_brancher | 1 + bin/destroy_brancher | 1 + hypernode_api_python/client.py | 2 - hypernode_api_python/commands.py | 60 +++++++++++++++++++++++++ tests/commands/test_create_brancher.py | 40 +++++++++++++++++ tests/commands/test_destroy_brancher.py | 53 ++++++++++++++++++++++ 6 files changed, 155 insertions(+), 2 deletions(-) create mode 120000 bin/create_brancher create mode 120000 bin/destroy_brancher create mode 100644 tests/commands/test_create_brancher.py create mode 100644 tests/commands/test_destroy_brancher.py diff --git a/bin/create_brancher b/bin/create_brancher new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/create_brancher @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/bin/destroy_brancher b/bin/destroy_brancher new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/destroy_brancher @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 1eb214f..1265202 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -840,7 +840,6 @@ def get_active_branchers(self, app_name): "GET", HYPERNODE_API_BRANCHER_APP_ENDPOINT.format(app_name) ) - # TODO: add entrypoint for this method in bin/ and commands.py def create_brancher(self, app_name, data): """ Create a new branch (server replica) of your Hypernode. @@ -854,7 +853,6 @@ def create_brancher(self, app_name, data): "POST", HYPERNODE_API_BRANCHER_APP_ENDPOINT.format(app_name), data=data ) - # TODO: add entrypoint for this method in bin/ and commands.py def destroy_brancher(self, brancher_name): """ Destroy an existing brancher node of your Hypernode. diff --git a/hypernode_api_python/commands.py b/hypernode_api_python/commands.py index 131b87a..54e3d12 100644 --- a/hypernode_api_python/commands.py +++ b/hypernode_api_python/commands.py @@ -641,3 +641,63 @@ def get_active_branchers(args=None): client = get_client() app_name = get_app_name() print_response(client.get_active_branchers(app_name)) + + +def create_brancher(args=None): + parser = ArgumentParser( + description=""" +Create a Brancher Hypernode from the specified app. +Outputs the app_info of the brancher to be created.. + +Example: +$ ./bin/create_brancher +{ + "name": "yourappname-ephoj82yb", + "parent": "yourappname", + "type": "brancher", + "product": "FALCON_M_202203", + "domainname": "yourappname-ephoj82yb.hypernode.io", + ... +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + data = {} + print_response(client.create_brancher(app_name, data=data)) + + +def destroy_brancher(args=None): + parser = ArgumentParser( + description=""" +Destroy a Brancher Hypernode. + +Examples: +$ ./bin/destroy_brancher yourbrancherappname-eph12345 +A job has been posted to cancel the 'yourbrancherappname-eph12345' brancher app. +""", + formatter_class=RawTextHelpFormatter, + ) + parser.add_argument( + "brancher_app_name", + help="The name of the brancher to destroy. See ./bin/get_active_branchers", + ) + args = parser.parse_args(args=args) + client = get_client() + try: + client.destroy_brancher(args.brancher_app_name) + print( + "A job has been posted to cancel the '{}' brancher app.".format( + args.brancher_app_name + ) + ) + exit(EX_OK) + except Exception as e: + print( + "Brancher app '{}' failed to be cancelled: {}".format( + args.brancher_app_name, e + ) + ) + exit(EX_UNAVAILABLE) diff --git a/tests/commands/test_create_brancher.py b/tests/commands/test_create_brancher.py new file mode 100644 index 0000000..d198775 --- /dev/null +++ b/tests/commands/test_create_brancher.py @@ -0,0 +1,40 @@ +from hypernode_api_python.commands import create_brancher +from tests.testcase import TestCase + + +class TestCreateBrancher(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_create_brancher_gets_client(self): + create_brancher([]) + + self.get_client.assert_called_once_with() + + def test_create_brancher_gets_app_name(self): + create_brancher([]) + + self.get_app_name.assert_called_once_with() + + def test_create_brancher_creates_brancher(self): + create_brancher([]) + + expected_data = {} + self.client.create_brancher.assert_called_once_with( + "myappname", data=expected_data + ) + + def test_create_brancher_prints_response(self): + create_brancher([]) + + self.print_response.assert_called_once_with( + self.client.create_brancher.return_value + ) diff --git a/tests/commands/test_destroy_brancher.py b/tests/commands/test_destroy_brancher.py new file mode 100644 index 0000000..56bfc83 --- /dev/null +++ b/tests/commands/test_destroy_brancher.py @@ -0,0 +1,53 @@ +from os import EX_UNAVAILABLE, EX_OK + +from hypernode_api_python.commands import destroy_brancher +from tests.testcase import TestCase + + +class TestDestroyBrancher(TestCase): + def setUp(self): + self.exit = self.set_up_patch("hypernode_api_python.commands.exit") + self.print = self.set_up_patch("hypernode_api_python.commands.print") + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + + def test_destroy_brancher_gets_client(self): + destroy_brancher(["myappname-eph1234"]) + + self.get_client.assert_called_once_with() + + def test_destroy_brancher_destroys_brancher(self): + destroy_brancher(["myappname-eph1234"]) + + self.client.destroy_brancher.assert_called_once_with("myappname-eph1234") + + def test_destroy_brancher_prints_app_name_is_valid(self): + destroy_brancher(["myappname-eph1234"]) + + self.print.assert_called_once_with( + "A job has been posted to cancel the 'myappname-eph1234' brancher app." + ) + + def test_destroy_brancher_prints_app_name_is_invalid(self): + self.client.destroy_brancher.side_effect = MemoryError("Killed") + + destroy_brancher(["myappname-eph1234"]) + + self.print.assert_called_once_with( + "Brancher app 'myappname-eph1234' failed to be cancelled: Killed" + ) + + def test_destroy_brancher_exits_zero_when_cancel_job_posted(self): + destroy_brancher(["myappname-eph1234"]) + + self.exit.assert_called_once_with(EX_OK) + + def test_destroy_brancher_exits_nonzero_when_unexpected_exception_occurs(self): + self.client.destroy_brancher.side_effect = RuntimeError + + destroy_brancher(["myappname-eph1234"]) + + self.exit.assert_called_once_with(EX_UNAVAILABLE) From 99ef7720d7839ba0c0ffcbf2199e89540d03c5bc Mon Sep 17 00:00:00 2001 From: Rick van de Loo Date: Tue, 28 May 2024 19:45:12 +0200 Subject: [PATCH 38/38] add bin/get_fpm_status looks like: ``` hypernode-api-python]$ ./bin/get_fpm_status --help usage: get_fpm_status [-h] Show the status of the PHP-FPM workers. Example: $ ./bin/get_fpm_status { "message": null, "data": "50570 IDLE 0.0s - phpfpm 127.0.0.1 GET magweb/status.php (python-requests/2.28.1) 50571 IDLE 0.0s - phpfpm 127.0.0.1 GET magweb/status.php (python-requests/2.28.1) ", "status": 200 } ``` --- bin/get_fpm_status | 1 + hypernode_api_python/client.py | 19 +++++++++++++ hypernode_api_python/commands.py | 21 +++++++++++++++ tests/client/test_get_fpm_status.py | 39 +++++++++++++++++++++++++++ tests/commands/test_get_fpm_status.py | 32 ++++++++++++++++++++++ 5 files changed, 112 insertions(+) create mode 120000 bin/get_fpm_status create mode 100644 tests/client/test_get_fpm_status.py create mode 100644 tests/commands/test_get_fpm_status.py diff --git a/bin/get_fpm_status b/bin/get_fpm_status new file mode 120000 index 0000000..e42252a --- /dev/null +++ b/bin/get_fpm_status @@ -0,0 +1 @@ +command \ No newline at end of file diff --git a/hypernode_api_python/client.py b/hypernode_api_python/client.py index 1265202..942f38e 100644 --- a/hypernode_api_python/client.py +++ b/hypernode_api_python/client.py @@ -21,6 +21,7 @@ HYPERNODE_API_BLOCK_ATTACK_ENDPOINT = "/v2/app/{}/block_attack/" HYPERNODE_API_BLOCK_ATTACK_DESCRIPTION_ENDPOINT = "/v2/app/block_attack_descriptions/" HYPERNODE_API_BRANCHER_APP_ENDPOINT = "/v2/brancher/app/{}/" +HYPERNODE_API_FPM_STATUS_APP_ENDPOINT = "/v2/nats/{}/hypernode.show-fpm-status" HYPERNODE_API_BRANCHER_ENDPOINT = "/v2/brancher/{}/" HYPERNODE_API_PRODUCT_APP_DETAIL_ENDPOINT = "/v2/product/app/{}/current/" HYPERNODE_API_PRODUCT_LIST_ENDPOINT = "/v2/product/" @@ -840,6 +841,24 @@ def get_active_branchers(self, app_name): "GET", HYPERNODE_API_BRANCHER_APP_ENDPOINT.format(app_name) ) + def get_fpm_status(self, app_name): + """ + Get the status of the FPM service for the specified app + Example: + > client.get_fpm_status("yourhypernodeappname").json() + { + "message": null, + "data": "50570 IDLE 0.0s - phpfpm 127.0.0.1 GET magweb/status.php (python-requests/2.28.1)\n50571 IDLE 0.0s - phpfpm 127.0.0.1 GET magweb/status.php (python-requests/2.28.1)\n", + "status": 200 + } + + :param str app_name: The name of the Hypernode to get the FPM status for + :return obj response: The request response object + """ + return self.requests( + "POST", HYPERNODE_API_FPM_STATUS_APP_ENDPOINT.format(app_name) + ) + def create_brancher(self, app_name, data): """ Create a new branch (server replica) of your Hypernode. diff --git a/hypernode_api_python/commands.py b/hypernode_api_python/commands.py index 54e3d12..7f96f6b 100644 --- a/hypernode_api_python/commands.py +++ b/hypernode_api_python/commands.py @@ -701,3 +701,24 @@ def destroy_brancher(args=None): ) ) exit(EX_UNAVAILABLE) + + +def get_fpm_status(args=None): + parser = ArgumentParser( + description=""" +Show the status of the PHP-FPM workers. + +Example: +$ ./bin/get_fpm_status +{ + "message": null, + "data": "50570 IDLE 0.0s - phpfpm 127.0.0.1 GET magweb/status.php (python-requests/2.28.1)\n50571 IDLE 0.0s - phpfpm 127.0.0.1 GET magweb/status.php (python-requests/2.28.1)\n", + "status": 200 +} +""", + formatter_class=RawTextHelpFormatter, + ) + parser.parse_args(args=args) + client = get_client() + app_name = get_app_name() + print_response(client.get_fpm_status(app_name)) diff --git a/tests/client/test_get_fpm_status.py b/tests/client/test_get_fpm_status.py new file mode 100644 index 0000000..8f54034 --- /dev/null +++ b/tests/client/test_get_fpm_status.py @@ -0,0 +1,39 @@ +from unittest import TestCase +from unittest.mock import Mock + +from hypernode_api_python.client import ( + HypernodeAPIPython, + HYPERNODE_API_FPM_STATUS_APP_ENDPOINT, +) + + +class TestGetFPMStatus(TestCase): + def setUp(self): + self.client = HypernodeAPIPython(token="my_token") + self.mock_request = Mock() + self.client.requests = self.mock_request + self.app_name = "my_app" + + def test_get_fpm_status_endpoint_is_correct(self): + self.assertEqual( + "/v2/nats/{}/hypernode.show-fpm-status", + HYPERNODE_API_FPM_STATUS_APP_ENDPOINT, + ) + + def test_calls_fpm_status_endpoint_properly(self): + self.client.get_fpm_status(self.app_name) + + self.mock_request.assert_called_once_with( + "POST", + f"/v2/nats/{self.app_name}/hypernode.show-fpm-status", + ) + + def test_returns_fpm_status_data(self): + response = Mock() + response.status_code = 200 + self.mock_request.return_value = response + + self.assertEqual( + self.client.get_fpm_status(self.app_name), + self.mock_request.return_value, + ) diff --git a/tests/commands/test_get_fpm_status.py b/tests/commands/test_get_fpm_status.py new file mode 100644 index 0000000..88a19a2 --- /dev/null +++ b/tests/commands/test_get_fpm_status.py @@ -0,0 +1,32 @@ +from hypernode_api_python.commands import get_fpm_status +from tests.testcase import TestCase + + +class TestGetFPMStatus(TestCase): + def setUp(self): + self.print_response = self.set_up_patch( + "hypernode_api_python.commands.print_response" + ) + self.get_client = self.set_up_patch("hypernode_api_python.commands.get_client") + self.client = self.get_client.return_value + self.get_app_name = self.set_up_patch( + "hypernode_api_python.commands.get_app_name" + ) + self.get_app_name.return_value = "myappname" + + def test_get_fpm_status_gets_client(self): + get_fpm_status([]) + + self.get_client.assert_called_once_with() + + def test_get_fpm_status_gets_fpm_status(self): + get_fpm_status([]) + + self.client.get_fpm_status.assert_called_once_with("myappname") + + def test_get_fpm_status_prints_fpm_status(self): + get_fpm_status([]) + + self.print_response.assert_called_once_with( + self.client.get_fpm_status.return_value + )