From 22ebc0ebac86fede00aee43e75ef7f6a0e56b301 Mon Sep 17 00:00:00 2001
From: matusdrobuliak66 <60785969+matusdrobuliak66@users.noreply.github.com>
Date: Mon, 13 Nov 2023 11:45:52 +0100
Subject: [PATCH 01/24] =?UTF-8?q?=E2=9C=A8=20listen=20to=20wallet=20events?=
=?UTF-8?q?=20in=20payment=20system=20(#5012)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/servicelib/rabbitmq/_client.py | 14 +++-
.../tests/rabbitmq/test_rabbitmq.py | 17 ++++
.../core/application.py | 4 +
.../services/auto_recharge_listener.py | 47 +++++++++++
.../services/auto_recharge_process_message.py | 20 +++++
.../services/rabbitmq.py | 2 +
services/payments/tests/unit/conftest.py | 3 +
.../test_services_auto_recharge_listener.py | 80 +++++++++++++++++++
.../wallets/payments/test_payments_methods.py | 1 -
9 files changed, 186 insertions(+), 2 deletions(-)
create mode 100644 services/payments/src/simcore_service_payments/services/auto_recharge_listener.py
create mode 100644 services/payments/src/simcore_service_payments/services/auto_recharge_process_message.py
create mode 100644 services/payments/tests/unit/test_services_auto_recharge_listener.py
diff --git a/packages/service-library/src/servicelib/rabbitmq/_client.py b/packages/service-library/src/servicelib/rabbitmq/_client.py
index ae668f26737..542bd2ba2ee 100644
--- a/packages/service-library/src/servicelib/rabbitmq/_client.py
+++ b/packages/service-library/src/servicelib/rabbitmq/_client.py
@@ -63,6 +63,9 @@ async def _get_channel(self) -> aio_pika.abc.AbstractChannel:
channel.close_callbacks.add(self._channel_close_callback)
return channel
+ async def _get_consumer_tag(self, exchange_name) -> str:
+ return f"{get_rabbitmq_client_unique_name(self.client_name)}_{exchange_name}"
+
async def subscribe(
self,
exchange_name: str,
@@ -139,10 +142,11 @@ async def _on_message(
)
await message.nack()
+ _consumer_tag = await self._get_consumer_tag(exchange_name)
await queue.consume(
_on_message,
exclusive=exclusive_queue,
- consumer_tag=f"{get_rabbitmq_client_unique_name(self.client_name)}_{exchange_name}",
+ consumer_tag=_consumer_tag,
)
output: str = queue.name
return output
@@ -214,3 +218,11 @@ async def publish(self, exchange_name: str, message: RabbitMessage) -> None:
aio_pika.Message(message.body()),
routing_key=message.routing_key() or "",
)
+
+ async def unsubscribe_consumer(self, exchange_name: str):
+ assert self._channel_pool # nosec
+ async with self._channel_pool.acquire() as channel:
+ queue_name = exchange_name
+ queue = await channel.get_queue(queue_name)
+ _consumer_tag = await self._get_consumer_tag(exchange_name)
+ await queue.cancel(_consumer_tag)
diff --git a/packages/service-library/tests/rabbitmq/test_rabbitmq.py b/packages/service-library/tests/rabbitmq/test_rabbitmq.py
index bf95e267d7c..af93023ea5f 100644
--- a/packages/service-library/tests/rabbitmq/test_rabbitmq.py
+++ b/packages/service-library/tests/rabbitmq/test_rabbitmq.py
@@ -435,3 +435,20 @@ async def test_rabbit_not_using_the_same_exchange_type_raises(
# now do a second subscribtion wiht topics, will create a TOPICS exchange
with pytest.raises(aio_pika.exceptions.ChannelPreconditionFailed):
await client.subscribe(exchange_name, mocked_message_parser, topics=[])
+
+
+@pytest.mark.no_cleanup_check_rabbitmq_server_has_no_errors()
+async def test_unsubscribe_consumer(
+ rabbitmq_client: Callable[[str], RabbitMQClient],
+ random_exchange_name: Callable[[], str],
+ mocked_message_parser: mock.AsyncMock,
+):
+ exchange_name = f"{random_exchange_name()}"
+ client = rabbitmq_client("consumer")
+ await client.subscribe(exchange_name, mocked_message_parser, exclusive_queue=False)
+ # Unsubsribe just a consumer, the queue will be still there
+ await client.unsubscribe_consumer(exchange_name)
+ # Unsubsribe the queue
+ await client.unsubscribe(exchange_name)
+ with pytest.raises(aio_pika.exceptions.ChannelNotFoundEntity):
+ await client.unsubscribe(exchange_name)
diff --git a/services/payments/src/simcore_service_payments/core/application.py b/services/payments/src/simcore_service_payments/core/application.py
index 899f2735d0f..815355b1cf9 100644
--- a/services/payments/src/simcore_service_payments/core/application.py
+++ b/services/payments/src/simcore_service_payments/core/application.py
@@ -11,6 +11,7 @@
)
from ..api.rest.routes import setup_rest_api
from ..api.rpc.routes import setup_rpc_api_routes
+from ..services.auto_recharge_listener import setup_auto_recharge_listener
from ..services.payments_gateway import setup_payments_gateway
from ..services.postgres import setup_postgres
from ..services.rabbitmq import setup_rabbitmq
@@ -51,6 +52,9 @@ def create_app(settings: ApplicationSettings | None = None) -> FastAPI:
# APIs w/ RUT
setup_resource_usage_tracker(app)
+ # Listening to Rabbitmq
+ setup_auto_recharge_listener(app)
+
# ERROR HANDLERS
# ... add here ...
diff --git a/services/payments/src/simcore_service_payments/services/auto_recharge_listener.py b/services/payments/src/simcore_service_payments/services/auto_recharge_listener.py
new file mode 100644
index 00000000000..d60c22d84f3
--- /dev/null
+++ b/services/payments/src/simcore_service_payments/services/auto_recharge_listener.py
@@ -0,0 +1,47 @@
+import functools
+import logging
+
+from fastapi import FastAPI
+from models_library.rabbitmq_messages import WalletCreditsMessage
+from servicelib.logging_utils import log_context
+from servicelib.rabbitmq import RabbitMQClient
+
+from .auto_recharge_process_message import process_message
+from .rabbitmq import get_rabbitmq_client
+
+_logger = logging.getLogger(__name__)
+
+
+async def _subscribe_to_rabbitmq(app) -> str:
+ with log_context(_logger, logging.INFO, msg="Subscribing to rabbitmq channel"):
+ rabbit_client: RabbitMQClient = get_rabbitmq_client(app)
+ subscribed_queue: str = await rabbit_client.subscribe(
+ WalletCreditsMessage.get_channel_name(),
+ message_handler=functools.partial(process_message, app),
+ exclusive_queue=False,
+ topics=["#"],
+ )
+ return subscribed_queue
+
+
+async def _unsubscribe_consumer(app) -> None:
+ with log_context(_logger, logging.INFO, msg="Unsubscribing from rabbitmq queue"):
+ rabbit_client: RabbitMQClient = get_rabbitmq_client(app)
+ await rabbit_client.unsubscribe_consumer(
+ WalletCreditsMessage.get_channel_name(),
+ )
+
+
+def setup_auto_recharge_listener(app: FastAPI):
+ async def _on_startup():
+ app.state.auto_recharge_rabbitmq_consumer = await _subscribe_to_rabbitmq(app)
+
+ async def _on_shutdown():
+ assert app.state.auto_recharge_rabbitmq_consumer # nosec
+ if app.state.rabbitmq_client:
+ # NOTE: We want to have persistent queue, therefore we will unsubscribe only consumer
+ await _unsubscribe_consumer(app)
+ app.state.auto_recharge_rabbitmq_constumer = None
+
+ app.add_event_handler("startup", _on_startup)
+ app.add_event_handler("shutdown", _on_shutdown)
diff --git a/services/payments/src/simcore_service_payments/services/auto_recharge_process_message.py b/services/payments/src/simcore_service_payments/services/auto_recharge_process_message.py
new file mode 100644
index 00000000000..8480063b19b
--- /dev/null
+++ b/services/payments/src/simcore_service_payments/services/auto_recharge_process_message.py
@@ -0,0 +1,20 @@
+import logging
+
+from fastapi import FastAPI
+from models_library.rabbitmq_messages import WalletCreditsMessage
+from pydantic import parse_raw_as
+
+_logger = logging.getLogger(__name__)
+
+
+async def process_message(app: FastAPI, data: bytes) -> bool:
+ assert app # nosec
+ rabbit_message = parse_raw_as(WalletCreditsMessage, data)
+ _logger.debug("Process msg: %s", rabbit_message)
+
+ # 1. Check if auto-recharge functionality is ON for wallet_id
+ # 2. Check if wallet credits are bellow the threshold
+ # 3. Get Payment method
+ # 4. Pay with payment method
+
+ return True
diff --git a/services/payments/src/simcore_service_payments/services/rabbitmq.py b/services/payments/src/simcore_service_payments/services/rabbitmq.py
index 5083c4f6b8a..58facd39868 100644
--- a/services/payments/src/simcore_service_payments/services/rabbitmq.py
+++ b/services/payments/src/simcore_service_payments/services/rabbitmq.py
@@ -31,8 +31,10 @@ async def _on_startup() -> None:
async def _on_shutdown() -> None:
if app.state.rabbitmq_client:
await app.state.rabbitmq_client.close()
+ app.state.rabbitmq_client = None
if app.state.rabbitmq_rpc_server:
await app.state.rabbitmq_rpc_server.close()
+ app.state.rabbitmq_rpc_server = None
app.add_event_handler("startup", _on_startup)
app.add_event_handler("shutdown", _on_shutdown)
diff --git a/services/payments/tests/unit/conftest.py b/services/payments/tests/unit/conftest.py
index f177e747db8..b46bc3a4673 100644
--- a/services/payments/tests/unit/conftest.py
+++ b/services/payments/tests/unit/conftest.py
@@ -57,6 +57,9 @@ def _do():
# The following services are affected if rabbitmq is not in place
mocker.patch("simcore_service_payments.core.application.setup_rabbitmq")
mocker.patch("simcore_service_payments.core.application.setup_rpc_api_routes")
+ mocker.patch(
+ "simcore_service_payments.core.application.setup_auto_recharge_listener"
+ )
return _do
diff --git a/services/payments/tests/unit/test_services_auto_recharge_listener.py b/services/payments/tests/unit/test_services_auto_recharge_listener.py
new file mode 100644
index 00000000000..9557dba382d
--- /dev/null
+++ b/services/payments/tests/unit/test_services_auto_recharge_listener.py
@@ -0,0 +1,80 @@
+# pylint: disable=protected-access
+# pylint: disable=redefined-outer-name
+# pylint: disable=too-many-arguments
+# pylint: disable=unused-argument
+# pylint: disable=unused-variable
+
+from collections.abc import Callable
+from decimal import Decimal
+from unittest import mock
+
+import pytest
+from fastapi import FastAPI
+from models_library.rabbitmq_messages import WalletCreditsMessage
+from pytest_mock.plugin import MockerFixture
+from pytest_simcore.helpers.typing_env import EnvVarsDict
+from pytest_simcore.helpers.utils_envs import setenvs_from_dict
+from servicelib.rabbitmq import RabbitMQClient
+from tenacity._asyncio import AsyncRetrying
+from tenacity.retry import retry_if_exception_type
+from tenacity.stop import stop_after_delay
+from tenacity.wait import wait_fixed
+
+pytest_simcore_core_services_selection = [
+ "postgres",
+ "rabbit",
+]
+pytest_simcore_ops_services_selection = [
+ "adminer",
+]
+
+
+@pytest.fixture
+def app_environment(
+ monkeypatch: pytest.MonkeyPatch,
+ app_environment: EnvVarsDict,
+ rabbit_env_vars_dict: EnvVarsDict, # rabbitMQ settings from 'rabbit' service
+ postgres_env_vars_dict: EnvVarsDict,
+ wait_for_postgres_ready_and_db_migrated: None,
+ external_environment: EnvVarsDict,
+):
+ # set environs
+ monkeypatch.delenv("PAYMENTS_RABBITMQ", raising=False)
+ monkeypatch.delenv("PAYMENTS_POSTGRES", raising=False)
+
+ return setenvs_from_dict(
+ monkeypatch,
+ {
+ **app_environment,
+ **rabbit_env_vars_dict,
+ **postgres_env_vars_dict,
+ **external_environment,
+ "POSTGRES_CLIENT_NAME": "payments-service-pg-client",
+ },
+ )
+
+
+@pytest.fixture
+async def mocked_message_parser(mocker: MockerFixture) -> mock.AsyncMock:
+ return mocker.patch(
+ "simcore_service_payments.services.auto_recharge_listener.process_message"
+ )
+
+
+async def test_process_event_functions(
+ mocked_message_parser: mock.AsyncMock,
+ app: FastAPI,
+ rabbitmq_client: Callable[[str], RabbitMQClient],
+):
+ publisher = rabbitmq_client("publisher")
+ msg = WalletCreditsMessage(wallet_id=1, credits=Decimal(120.5))
+ await publisher.publish(WalletCreditsMessage.get_channel_name(), msg)
+
+ async for attempt in AsyncRetrying(
+ wait=wait_fixed(0.1),
+ stop=stop_after_delay(5),
+ retry=retry_if_exception_type(AssertionError),
+ reraise=True,
+ ):
+ with attempt:
+ mocked_message_parser.assert_called_once()
diff --git a/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments_methods.py b/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments_methods.py
index 500ede13316..9b1fe418560 100644
--- a/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments_methods.py
+++ b/services/web/server/tests/unit/with_dbs/03/wallets/payments/test_payments_methods.py
@@ -334,7 +334,6 @@ async def wallet_payment_method_id(
return await _add_payment_method(client, wallet_id=logged_user_wallet.wallet_id)
-@pytest.mark.testit
async def test_one_time_payment_with_payment_method(
latest_osparc_price: Decimal,
client: TestClient,
From 0284b248a212861d0143a9a5cba4c26d136da232 Mon Sep 17 00:00:00 2001
From: Ignacio Pascual <4764217+ignapas@users.noreply.github.com>
Date: Mon, 13 Nov 2023 12:52:15 +0100
Subject: [PATCH 02/24] Share wallet bug (#5021)
Fixes #5006
---
.../client/source/class/osparc/desktop/wallets/MembersList.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/static-webserver/client/source/class/osparc/desktop/wallets/MembersList.js b/services/static-webserver/client/source/class/osparc/desktop/wallets/MembersList.js
index 9104752a285..2a827c0fc99 100644
--- a/services/static-webserver/client/source/class/osparc/desktop/wallets/MembersList.js
+++ b/services/static-webserver/client/source/class/osparc/desktop/wallets/MembersList.js
@@ -132,7 +132,7 @@ qx.Class.define("osparc.desktop.wallets.MembersList", {
serializedData["resourceType"] = "wallet";
const showOrganizations = false;
const collaboratorsManager = new osparc.share.NewCollaboratorsManager(serializedData, showOrganizations);
- collaboratorsManager.addListener("addCollaborators", e => {
+ collaboratorsManager.addListener("addEditors", e => {
const cb = () => collaboratorsManager.close();
this.__addMembers(e.getData(), cb);
}, this);
From 6f515976f7bd49651f4064762d5e0c6920f77c09 Mon Sep 17 00:00:00 2001
From: Sylvain <35365065+sanderegg@users.noreply.github.com>
Date: Mon, 13 Nov 2023 18:08:58 +0100
Subject: [PATCH 03/24] =?UTF-8?q?=E2=99=BB=EF=B8=8FAutoscaling:=20Debug=20?=
=?UTF-8?q?logs=20for=20issue=20with=20scaling=20up=20(#5025)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../utils/utils_docker.py | 33 +++++++++++-------
services/autoscaling/tests/unit/conftest.py | 1 +
.../unit/test_modules_auto_scaling_dynamic.py | 34 +++++++++++++++----
3 files changed, 48 insertions(+), 20 deletions(-)
diff --git a/services/autoscaling/src/simcore_service_autoscaling/utils/utils_docker.py b/services/autoscaling/src/simcore_service_autoscaling/utils/utils_docker.py
index 0e5e88585c2..65b8ce8af05 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/utils/utils_docker.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/utils/utils_docker.py
@@ -107,21 +107,24 @@ def _check_if_node_is_removable(node: Node) -> bool:
def _is_task_waiting_for_resources(task: Task) -> bool:
# NOTE: https://docs.docker.com/engine/swarm/how-swarm-mode-works/swarm-task-states/
- if (
- not task.Status
- or not task.Status.State
- or not task.Status.Message
- or not task.Status.Err
+ with log_context(
+ logger, level=logging.DEBUG, msg=f"_is_task_waiting_for_resources: {task}"
):
- return False
- return (
- task.Status.State == TaskState.pending
- and task.Status.Message == _PENDING_DOCKER_TASK_MESSAGE
- and (
- _INSUFFICIENT_RESOURCES_DOCKER_TASK_ERR in task.Status.Err
- or _NOT_SATISFIED_SCHEDULING_CONSTRAINTS_TASK_ERR in task.Status.Err
+ if (
+ not task.Status
+ or not task.Status.State
+ or not task.Status.Message
+ or not task.Status.Err
+ ):
+ return False
+ return (
+ task.Status.State == TaskState.pending
+ and task.Status.Message == _PENDING_DOCKER_TASK_MESSAGE
+ and (
+ _INSUFFICIENT_RESOURCES_DOCKER_TASK_ERR in task.Status.Err
+ or _NOT_SATISFIED_SCHEDULING_CONSTRAINTS_TASK_ERR in task.Status.Err
+ )
)
- )
async def _associated_service_has_no_node_placement_contraints(
@@ -187,6 +190,10 @@ async def pending_service_tasks_with_insufficient_resources(
)
sorted_tasks = sorted(tasks, key=_by_created_dt)
+ logger.debug(
+ "found following tasks that might trigger autoscaling: %s",
+ [task.ID for task in tasks],
+ )
return [
task
diff --git a/services/autoscaling/tests/unit/conftest.py b/services/autoscaling/tests/unit/conftest.py
index 619ec908545..e388acba225 100644
--- a/services/autoscaling/tests/unit/conftest.py
+++ b/services/autoscaling/tests/unit/conftest.py
@@ -514,6 +514,7 @@ def aws_allowed_ec2_instance_type_names() -> list[InstanceTypeType]:
"t2.2xlarge",
"g3.4xlarge",
"g4dn.2xlarge",
+ "g4dn.8xlarge",
"r5n.4xlarge",
"r5n.8xlarge",
]
diff --git a/services/autoscaling/tests/unit/test_modules_auto_scaling_dynamic.py b/services/autoscaling/tests/unit/test_modules_auto_scaling_dynamic.py
index 0044c157b0d..cb41d2d3278 100644
--- a/services/autoscaling/tests/unit/test_modules_auto_scaling_dynamic.py
+++ b/services/autoscaling/tests/unit/test_modules_auto_scaling_dynamic.py
@@ -379,7 +379,9 @@ async def _assert_ec2_instances(
assert len(all_instances["Reservations"]) == num_reservations
for reservation in all_instances["Reservations"]:
assert "Instances" in reservation
- assert len(reservation["Instances"]) == num_instances
+ assert (
+ len(reservation["Instances"]) == num_instances
+ ), f"created {num_instances} instances of {reservation['Instances'][0]['InstanceType'] if num_instances > 0 else 'n/a'}"
for instance in reservation["Instances"]:
assert "InstanceType" in instance
assert instance["InstanceType"] == instance_type
@@ -440,7 +442,7 @@ async def _assert_ec2_instances(
),
],
)
-async def test_cluster_scaling_up_and_down(
+async def test_cluster_scaling_up_and_down( # noqa: PLR0915
minimal_configuration: None,
service_monitored_labels: dict[DockerLabelKey, str],
app_settings: ApplicationSettings,
@@ -686,6 +688,7 @@ async def test_cluster_scaling_up_and_down(
@dataclass(frozen=True)
class _ScaleUpParams:
+ imposed_instance_type: str | None
service_resources: Resources
num_services: int
expected_instance_type: str
@@ -697,15 +700,28 @@ class _ScaleUpParams:
[
pytest.param(
_ScaleUpParams(
+ imposed_instance_type=None,
service_resources=Resources(
cpus=5, ram=parse_obj_as(ByteSize, "36Gib")
),
num_services=10,
- expected_instance_type="g3.4xlarge",
+ expected_instance_type="g3.4xlarge", # 1 GPU, 16 CPUs, 122GiB
expected_num_instances=4,
),
id="sim4life-light",
- )
+ ),
+ pytest.param(
+ _ScaleUpParams(
+ imposed_instance_type="g4dn.8xlarge",
+ service_resources=Resources(
+ cpus=5, ram=parse_obj_as(ByteSize, "20480MB")
+ ),
+ num_services=7,
+ expected_instance_type="g4dn.8xlarge", # 1 GPU, 32 CPUs, 128GiB
+ expected_num_instances=2,
+ ),
+ id="sim4life",
+ ),
],
)
async def test_cluster_scaling_up_starts_multiple_instances(
@@ -714,13 +730,12 @@ async def test_cluster_scaling_up_starts_multiple_instances(
app_settings: ApplicationSettings,
initialized_app: FastAPI,
create_service: Callable[
- [dict[str, Any], dict[DockerLabelKey, str], str], Awaitable[Service]
+ [dict[str, Any], dict[DockerLabelKey, str], str, list[str]], Awaitable[Service]
],
task_template: dict[str, Any],
create_task_reservations: Callable[[int, int], dict[str, Any]],
ec2_client: EC2Client,
mock_tag_node: mock.Mock,
- fake_node: Node,
scale_up_params: _ScaleUpParams,
mock_rabbitmq_post_message: mock.Mock,
mock_find_node_with_name: mock.Mock,
@@ -741,6 +756,11 @@ async def test_cluster_scaling_up_starts_multiple_instances(
),
service_monitored_labels,
"pending",
+ [
+ f"node.labels.{DOCKER_TASK_EC2_INSTANCE_TYPE_PLACEMENT_CONSTRAINT_KEY}=={scale_up_params.imposed_instance_type}"
+ ]
+ if scale_up_params.imposed_instance_type
+ else [],
)
for _ in range(scale_up_params.num_services)
)
@@ -756,7 +776,7 @@ async def test_cluster_scaling_up_starts_multiple_instances(
ec2_client,
num_reservations=1,
num_instances=scale_up_params.expected_num_instances,
- instance_type="g3.4xlarge",
+ instance_type=scale_up_params.expected_instance_type,
instance_state="running",
)
From 9cabec07f034fa1bd1e76dacbba54e07b02f39bb Mon Sep 17 00:00:00 2001
From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com>
Date: Mon, 13 Nov 2023 22:27:34 +0100
Subject: [PATCH 04/24] =?UTF-8?q?=F0=9F=93=9D=20Maintenance:=20cleanup=20r?=
=?UTF-8?q?eadme=20and=20vscode=20settings=20(#5023)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.vscode/extensions.json | 4 ++++
.vscode/settings.template.json | 26 ++++++++------------------
README.md | 12 ++++--------
3 files changed, 16 insertions(+), 26 deletions(-)
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 24bd21ba4e7..65f20197c67 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -3,9 +3,13 @@
"charliermarsh.ruff",
"eamodio.gitlens",
"exiasr.hadolint",
+ "ms-azuretools.vscode-docker",
+ "ms-python.black-formatter",
+ "ms-python.pylint",
"ms-python.python",
"njpwerner.autodocstring",
"samuelcolvin.jinjahtml",
"timonwong.shellcheck",
+ "vscode-icons-team.vscode-icons",
]
}
diff --git a/.vscode/settings.template.json b/.vscode/settings.template.json
index 731b7d3b0f8..3a4405e9594 100644
--- a/.vscode/settings.template.json
+++ b/.vscode/settings.template.json
@@ -1,5 +1,7 @@
// This is a template. Clone and replace extension ".template.json" by ".json"
{
+ "autoDocstring.docstringFormat": "pep257",
+
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.detectIndentation": false,
@@ -27,9 +29,8 @@
"**/.git/subtree-cache/**": true,
"**/node_modules/*/**": true
},
- "python.formatting.autopep8Args": [
- "--max-line-length 140"
- ],
+ "python.analysis.autoImportCompletions": true,
+ "python.analysis.typeCheckingMode": "basic",
"python.analysis.extraPaths": [
"./packages/models-library/src",
"./packages/postgres-database/src",
@@ -54,26 +55,15 @@
"[makefile]": {
"editor.insertSpaces": false
},
- "python.testing.pytestEnabled": true,
- "autoDocstring.docstringFormat": "pep257",
"hadolint.hadolintPath": "${workspaceFolder}/scripts/hadolint.bash",
"hadolint.cliOptions": [],
- "shellcheck.executablePath": "${workspaceFolder}/scripts/shellcheck.bash",
- "shellcheck.run": "onSave",
- "shellcheck.enableQuickFix": true,
- "python.formatting.provider": "black",
- "isort.path": [
- "${workspaceFolder}/.venv/bin/isort"
- ],
- "isort.args": [
- "--settings-path=${workspaceFolder}/.isort.cfg"
- ],
- "python.analysis.autoImportCompletions": true,
- "python.analysis.typeCheckingMode": "basic",
"ruff.lint.args": [
"--config=${workspaceFolder}/.ruff.toml"
],
"ruff.path": [
"${workspaceFolder}/.venv/bin/ruff"
- ]
+ ],
+ "shellcheck.executablePath": "${workspaceFolder}/scripts/shellcheck.bash",
+ "shellcheck.run": "onSave",
+ "shellcheck.enableQuickFix": true
}
diff --git a/README.md b/README.md
index 02051728b81..1cb462938a5 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,6 @@
-
[![black_badge]](https://github.com/psf/black)
[![ci_badge]](https://github.com/ITISFoundation/osparc-simcore/actions/workflows/ci-testing-deploy.yml)
@@ -28,7 +27,6 @@
[osparc_status]:https://img.shields.io/badge/dynamic/json?label=osparc.io&query=%24.status.description&url=https%3A%2F%2Fstatus.osparc.io%2Fapi%2Fv2%2Fstatus.json
-
The SIM-CORE, named **o2S2PARC** – **O**pen **O**nline **S**imulations for **S**timulating **P**eripheral **A**ctivity to **R**elieve **C**onditions – is one of the three integrative cores of the SPARC program’s Data Resource Center (DRC).
The aim of o2S2PARC is to establish a comprehensive, freely accessible, intuitive, and interactive online platform for simulating peripheral nerve system neuromodulation/ stimulation and its impact on organ physiology in a precise and predictive manner.
To achieve this, the platform will comprise both state-of-the art and highly detailed animal and human anatomical models with realistic tissue property distributions that make it possible to perform simulations ranging from the molecular scale up to the complexity of the human body.
@@ -72,15 +70,14 @@ Services are deployed in two stacks:``simcore-stack`` comprises all core-service
To build and run:
- git
-- docker
+- [docker](https://docs.docker.com/engine/install/ubuntu/#installation-methods)
- make >=4.2
- awk, jq (optional tools within makefiles)
To develop, in addition:
-- python 3.10
-- nodejs for client part (this dependency will be deprecated soon)
-- swagger-cli (make sure to have a recent version of nodejs)
+- *python 3.10*: we recommend using the python manager [pyenv](https://brain2life.hashnode.dev/how-to-install-pyenv-python-version-manager-on-ubuntu-2004)
+- *nodejs* for client part: we recommend using the node manager [nvm](https://github.com/nvm-sh/nvm#install--update-script)
- [vscode] (highly recommended)
To verify current base OS, Docker and Python build versions have a look at:
@@ -135,7 +132,7 @@ To upgrade a single requirement named `fastapi`run:
- [Git release workflow](docs/releasing-workflow-instructions.md)
- Public [releases](https://github.com/ITISFoundation/osparc-simcore/releases)
-- Production in https://osparc.io
+- Production in
- [Staging instructions](docs/releasing-workflow-instructions.md#staging-example)
- [User Manual](https://itisfoundation.github.io/osparc-manual/)
@@ -191,6 +188,5 @@ This project is licensed under the terms of the [MIT license](LICENSE).
-[chocolatey]:https://chocolatey.org/
[vscode]:https://code.visualstudio.com/
[WSL2]:https://docs.microsoft.com/en-us/windows/wsl
From 4d75a0c3239d6d7adf203dc4cdcceedece4c7ca5 Mon Sep 17 00:00:00 2001
From: Andrei Neagu <5694077+GitHK@users.noreply.github.com>
Date: Tue, 14 Nov 2023 08:56:04 +0000
Subject: [PATCH 05/24] =?UTF-8?q?=F0=9F=90=9B=20adding=20missing=20env=20v?=
=?UTF-8?q?ar=20to=20docker-compose.yaml=20(#5011)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Andrei Neagu
---
.env-devel | 1 +
docs/env-vars.md | 10 ++++++++++
services/docker-compose.yml | 1 +
3 files changed, 12 insertions(+)
create mode 100644 docs/env-vars.md
diff --git a/.env-devel b/.env-devel
index 5bc2e21c741..f2e27bf1c32 100644
--- a/.env-devel
+++ b/.env-devel
@@ -47,6 +47,7 @@ DIRECTOR_V2_DEV_FEATURES_ENABLED=0
DYNAMIC_SIDECAR_IMAGE=${DOCKER_REGISTRY:-itisfoundation}/dynamic-sidecar:${DOCKER_IMAGE_TAG:-latest}
DYNAMIC_SIDECAR_LOG_LEVEL=DEBUG
DYNAMIC_SIDECAR_PROMETHEUS_SERVICE_LABELS={}
+DYNAMIC_SIDECAR_PROMETHEUS_MONITORING_NETWORKS=[]
FUNCTION_SERVICES_AUTHORS='{"UN": {"name": "Unknown", "email": "unknown@osparc.io", "affiliation": "unknown"}}'
diff --git a/docs/env-vars.md b/docs/env-vars.md
new file mode 100644
index 00000000000..7a389701565
--- /dev/null
+++ b/docs/env-vars.md
@@ -0,0 +1,10 @@
+# Environment variables management
+
+As a developer you will need to extend the current env vars for a service.
+
+The following rules must be followed:
+
+1. for each service that requires it, add it to the `services/docker-compose.yml` file (such as `MY_VAR=${MY_VAR}`)
+2. add a meaningful default value for development inside `.env-devel` so that developers can work, and that `osparc-simcore` is **self contained**.
+ - **NOTE** if the variable has a default inside the code, put the same value here
+3. inside the repo where devops keep all the secrets follow the instructions to add the new env var
diff --git a/services/docker-compose.yml b/services/docker-compose.yml
index 34fc67d1b02..0d37c5d002d 100644
--- a/services/docker-compose.yml
+++ b/services/docker-compose.yml
@@ -198,6 +198,7 @@ services:
- SIMCORE_SERVICES_NETWORK_NAME=interactive_services_subnet
- TRACING_THRIFT_COMPACT_ENDPOINT=${TRACING_THRIFT_COMPACT_ENDPOINT}
- DYNAMIC_SIDECAR_PROMETHEUS_SERVICE_LABELS=${DYNAMIC_SIDECAR_PROMETHEUS_SERVICE_LABELS}
+ - DYNAMIC_SIDECAR_PROMETHEUS_MONITORING_NETWORKS=${DYNAMIC_SIDECAR_PROMETHEUS_MONITORING_NETWORKS}
env_file:
- ../.env
volumes:
From 992d8be2f37b341bce72e0d748cecf8c416978ff Mon Sep 17 00:00:00 2001
From: Sylvain <35365065+sanderegg@users.noreply.github.com>
Date: Tue, 14 Nov 2023 14:33:00 +0100
Subject: [PATCH 06/24] =?UTF-8?q?=E2=9C=A8=20Re-introduce=20aws-library=20?=
=?UTF-8?q?(#5031)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/codeql/codeql-config.yml | 1 +
.github/workflows/ci-testing-deploy.yml | 47 ++
.vscode/settings.template.json | 1 +
ci/github/unit-testing/aws-library.bash | 41 ++
packages/aws-library/Makefile | 49 ++
packages/aws-library/README.md | 22 +
packages/aws-library/VERSION | 1 +
packages/aws-library/requirements/Makefile | 6 +
packages/aws-library/requirements/_base.in | 12 +
packages/aws-library/requirements/_base.txt | 200 ++++++++
packages/aws-library/requirements/_test.in | 25 +
packages/aws-library/requirements/_test.txt | 326 +++++++++++++
packages/aws-library/requirements/_tools.in | 5 +
packages/aws-library/requirements/_tools.txt | 92 ++++
packages/aws-library/requirements/ci.txt | 20 +
packages/aws-library/requirements/dev.txt | 20 +
packages/aws-library/setup.cfg | 17 +
packages/aws-library/setup.py | 58 +++
.../aws-library/src/aws_library/__init__.py | 3 +
.../src/aws_library/ec2/__init__.py | 0
.../aws-library/src/aws_library/ec2/client.py | 284 ++++++++++++
.../aws-library/src/aws_library/ec2/errors.py | 19 +
.../aws-library/src/aws_library/ec2/models.py | 76 ++++
.../aws-library/src/aws_library/ec2/utils.py | 10 +
packages/aws-library/tests/conftest.py | 23 +
packages/aws-library/tests/test_ec2_client.py | 430 ++++++++++++++++++
packages/aws-library/tests/test_ec2_models.py | 124 +++++
packages/aws-library/tests/test_ec2_utils.py | 7 +
.../ec2_instances.py | 2 +-
packages/pytest-simcore/setup.py | 1 +
.../src/pytest_simcore/aioresponses_mocker.py | 2 +-
.../src/pytest_simcore/aws_ec2_service.py | 149 ++++++
.../src/pytest_simcore/aws_server.py | 86 ++++
.../src/pytest_simcore/aws_services.py | 83 ----
.../src/pytest_simcore/docker_compose.py | 19 +-
.../src/pytest_simcore/docker_registry.py | 2 +-
.../src/pytest_simcore/docker_swarm.py | 2 +-
.../pytest_simcore/helpers/utils_docker.py | 14 -
.../src/pytest_simcore/helpers/utils_host.py | 14 +
.../src/pytest_simcore/httpbin_service.py | 2 +-
.../src/pytest_simcore/minio_service.py | 7 +-
.../src/pytest_simcore/postgres_service.py | 3 +-
.../src/pytest_simcore/rabbit_service.py | 3 +-
.../src/pytest_simcore/redis_service.py | 17 +-
.../src/pytest_simcore/simcore_services.py | 3 +-
.../pytest_simcore/simcore_storage_service.py | 7 +-
.../clusters_keeper/ec2_instances.py | 6 +-
.../src/settings_library/ec2.py | 26 ++
packages/simcore-sdk/tests/conftest.py | 2 +-
.../test_node_ports_common_file_io_utils.py | 6 +-
services/agent/requirements/_base.in | 1 +
services/agent/requirements/_base.txt | 2 +
services/agent/requirements/_test.txt | 8 +-
services/agent/tests/conftest.py | 6 +-
services/api-server/tests/unit/conftest.py | 3 +-
services/autoscaling/requirements/_base.in | 3 +-
services/autoscaling/requirements/_base.txt | 19 +-
services/autoscaling/requirements/ci.txt | 3 +-
services/autoscaling/requirements/dev.txt | 1 +
services/autoscaling/requirements/prod.txt | 1 +
.../core/settings.py | 10 +-
.../src/simcore_service_autoscaling/models.py | 59 +--
.../modules/auto_scaling_core.py | 39 +-
.../modules/auto_scaling_mode_base.py | 3 +-
.../auto_scaling_mode_computational.py | 10 +-
.../modules/auto_scaling_mode_dynamic.py | 3 +-
.../modules/dask.py | 10 +-
.../modules/ec2.py | 235 +---------
.../utils/auto_scaling_core.py | 6 +-
.../utils/computational_scaling.py | 9 +-
.../utils/dynamic_scaling.py | 3 +-
.../utils/rabbitmq.py | 3 +-
.../utils/utils_docker.py | 4 +-
.../utils/utils_ec2.py | 3 +-
services/autoscaling/tests/unit/conftest.py | 220 ++-------
.../autoscaling/tests/unit/test_api_health.py | 2 +-
.../tests/unit/test_core_settings.py | 14 +-
.../autoscaling/tests/unit/test_models.py | 84 +---
...test_modules_auto_scaling_computational.py | 30 +-
.../unit/test_modules_auto_scaling_dynamic.py | 27 +-
.../unit/test_modules_auto_scaling_task.py | 2 +-
.../tests/unit/test_modules_dask.py | 2 +-
.../tests/unit/test_modules_ec2.py | 329 +-------------
.../unit/test_utils_computational_scaling.py | 3 +-
.../tests/unit/test_utils_docker.py | 14 +-
.../tests/unit/test_utils_dynamic_scaling.py | 3 +-
.../autoscaling/tests/unit/test_utils_ec2.py | 8 +-
.../clusters-keeper/requirements/_base.in | 3 +-
.../clusters-keeper/requirements/_base.txt | 98 +++-
.../clusters-keeper/requirements/_test.txt | 4 +-
services/clusters-keeper/requirements/ci.txt | 1 +
services/clusters-keeper/requirements/dev.txt | 1 +
.../clusters-keeper/requirements/prod.txt | 1 +
services/clusters-keeper/setup.py | 1 +
.../core/settings.py | 28 +-
.../simcore_service_clusters_keeper/models.py | 19 -
.../modules/clusters.py | 44 +-
.../modules/clusters_management_core.py | 5 +-
.../modules/ec2.py | 286 +-----------
.../rpc/clusters.py | 2 +-
.../rpc/ec2_instances.py | 12 +-
.../utils/clusters.py | 10 +-
.../utils/dask.py | 3 +-
.../utils/ec2.py | 2 +-
.../clusters-keeper/tests/unit/conftest.py | 238 ++--------
.../tests/unit/test_api_health.py | 2 +-
.../tests/unit/test_modules_clusters.py | 11 +-
.../test_modules_clusters_management_core.py | 8 +-
.../test_modules_clusters_management_task.py | 2 +-
.../tests/unit/test_modules_ec2.py | 340 +-------------
.../tests/unit/test_rpc_clusters.py | 7 +-
.../tests/unit/test_rpc_ec2_instances.py | 10 +-
.../tests/unit/test_utils_clusters.py | 4 +-
.../tests/unit/test_utils_ec2.py | 5 +-
services/dask-sidecar/tests/unit/conftest.py | 2 +-
.../db/repositories/comp_tasks/_utils.py | 6 +-
.../02/test_dynamic_services_routes.py | 2 +-
...t_dynamic_sidecar_nodeports_integration.py | 2 +-
...ixed_dynamic_sidecar_and_legacy_project.py | 2 +-
.../director-v2/tests/integration/02/utils.py | 2 +-
.../with_dbs/test_api_route_computations.py | 6 +-
.../unit/test_modules_outputs_watcher.py | 1 +
.../tests/integration/conftest.py | 6 +-
.../tests/integration/test_clusters.py | 2 +-
.../tests/integration/test_dask_sidecar.py | 2 +-
.../tests/integration/test_gateway.py | 2 +-
.../tests/system/test_deploy.py | 2 +-
services/storage/tests/conftest.py | 2 +-
128 files changed, 2681 insertions(+), 2062 deletions(-)
create mode 100755 ci/github/unit-testing/aws-library.bash
create mode 100644 packages/aws-library/Makefile
create mode 100644 packages/aws-library/README.md
create mode 100644 packages/aws-library/VERSION
create mode 100644 packages/aws-library/requirements/Makefile
create mode 100644 packages/aws-library/requirements/_base.in
create mode 100644 packages/aws-library/requirements/_base.txt
create mode 100644 packages/aws-library/requirements/_test.in
create mode 100644 packages/aws-library/requirements/_test.txt
create mode 100644 packages/aws-library/requirements/_tools.in
create mode 100644 packages/aws-library/requirements/_tools.txt
create mode 100644 packages/aws-library/requirements/ci.txt
create mode 100644 packages/aws-library/requirements/dev.txt
create mode 100644 packages/aws-library/setup.cfg
create mode 100644 packages/aws-library/setup.py
create mode 100644 packages/aws-library/src/aws_library/__init__.py
create mode 100644 packages/aws-library/src/aws_library/ec2/__init__.py
create mode 100644 packages/aws-library/src/aws_library/ec2/client.py
create mode 100644 packages/aws-library/src/aws_library/ec2/errors.py
create mode 100644 packages/aws-library/src/aws_library/ec2/models.py
create mode 100644 packages/aws-library/src/aws_library/ec2/utils.py
create mode 100644 packages/aws-library/tests/conftest.py
create mode 100644 packages/aws-library/tests/test_ec2_client.py
create mode 100644 packages/aws-library/tests/test_ec2_models.py
create mode 100644 packages/aws-library/tests/test_ec2_utils.py
create mode 100644 packages/pytest-simcore/src/pytest_simcore/aws_ec2_service.py
create mode 100644 packages/pytest-simcore/src/pytest_simcore/aws_server.py
delete mode 100644 packages/pytest-simcore/src/pytest_simcore/aws_services.py
create mode 100644 packages/pytest-simcore/src/pytest_simcore/helpers/utils_host.py
create mode 100644 packages/settings-library/src/settings_library/ec2.py
delete mode 100644 services/clusters-keeper/src/simcore_service_clusters_keeper/models.py
diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml
index ace67457635..adac3b13795 100644
--- a/.github/codeql/codeql-config.yml
+++ b/.github/codeql/codeql-config.yml
@@ -3,6 +3,7 @@ name: "ospac-simcore CodeQL config"
disable-default-queries: false
paths:
+ - packages/aws-library/src
- packages/dask-task-models-library/src
- packages/models-library/src/models_library
- packages/postgres-database/src/simcore_postgres_database
diff --git a/.github/workflows/ci-testing-deploy.yml b/.github/workflows/ci-testing-deploy.yml
index 4f001b7fcb6..6e184986192 100644
--- a/.github/workflows/ci-testing-deploy.yml
+++ b/.github/workflows/ci-testing-deploy.yml
@@ -50,6 +50,7 @@ jobs:
runs-on: ubuntu-latest
# Set job outputs to values from filter step
outputs:
+ aws-library: ${{ steps.filter.outputs.aws-library }}
dask-task-models-library: ${{ steps.filter.outputs.dask-task-models-library }}
models-library: ${{ steps.filter.outputs.models-library }}
postgres-database: ${{ steps.filter.outputs.postgres-database }}
@@ -87,6 +88,12 @@ jobs:
id: filter
with:
filters: |
+ aws-library:
+ - 'packages/aws-library/**'
+ - 'packages/pytest-simcore/**'
+ - 'services/docker-compose*'
+ - 'scripts/mypy/*'
+ - 'mypy.ini'
dask-task-models-library:
- 'packages/dask-task-models-library/**'
- 'packages/pytest-simcore/**'
@@ -804,6 +811,45 @@ jobs:
with:
flags: unittests #optional
+ unit-test-aws-library:
+ needs: changes
+ if: ${{ needs.changes.outputs.aws-library == 'true' || github.event_name == 'push' }}
+ timeout-minutes: 18 # if this timeout gets too small, then split the tests
+ name: "[unit] aws-library"
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ python: ["3.10"]
+ os: [ubuntu-22.04]
+ docker_buildx: [v0.10.4]
+ fail-fast: false
+ steps:
+ - uses: actions/checkout@v4
+ - name: setup docker buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v3
+ with:
+ version: ${{ matrix.docker_buildx }}
+ driver: docker-container
+ - name: setup python environment
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python }}
+ cache: "pip"
+ cache-dependency-path: "packages/aws-library/requirements/ci.txt"
+ - name: show system version
+ run: ./ci/helpers/show_system_versions.bash
+ - name: install
+ run: ./ci/github/unit-testing/aws-library.bash install
+ - name: typecheck
+ run: ./ci/github/unit-testing/aws-library.bash typecheck
+ - name: test
+ if: always()
+ run: ./ci/github/unit-testing/aws-library.bash test
+ - uses: codecov/codecov-action@v3.1.4
+ with:
+ flags: unittests #optional
+
unit-test-dask-task-models-library:
needs: changes
if: ${{ needs.changes.outputs.dask-task-models-library == 'true' || github.event_name == 'push' }}
@@ -1447,6 +1493,7 @@ jobs:
unit-test-catalog,
unit-test-clusters-keeper,
unit-test-dask-sidecar,
+ unit-test-aws-library,
unit-test-dask-task-models-library,
unit-test-datcore-adapter,
unit-test-director-v2,
diff --git a/.vscode/settings.template.json b/.vscode/settings.template.json
index 3a4405e9594..8df39917def 100644
--- a/.vscode/settings.template.json
+++ b/.vscode/settings.template.json
@@ -32,6 +32,7 @@
"python.analysis.autoImportCompletions": true,
"python.analysis.typeCheckingMode": "basic",
"python.analysis.extraPaths": [
+ "./packages/aws-library/src",
"./packages/models-library/src",
"./packages/postgres-database/src",
"./packages/postgres-database/tests",
diff --git a/ci/github/unit-testing/aws-library.bash b/ci/github/unit-testing/aws-library.bash
new file mode 100755
index 00000000000..6250656e18f
--- /dev/null
+++ b/ci/github/unit-testing/aws-library.bash
@@ -0,0 +1,41 @@
+#!/bin/bash
+# http://redsymbol.net/articles/unofficial-bash-strict-mode/
+set -o errexit # abort on nonzero exitstatus
+set -o nounset # abort on unbound variable
+set -o pipefail # don't hide errors within pipes
+IFS=$'\n\t'
+
+install() {
+ bash ci/helpers/ensure_python_pip.bash
+ make devenv
+ # shellcheck source=/dev/null
+ source .venv/bin/activate
+ pushd packages/aws-library
+ make install-ci
+ popd
+ .venv/bin/pip list --verbose
+}
+
+test() {
+ # shellcheck source=/dev/null
+ source .venv/bin/activate
+ pushd packages/aws-library
+ make tests-ci
+ popd
+}
+
+typecheck() {
+ pushd packages/aws-library
+ make mypy
+ popd
+}
+
+# Check if the function exists (bash specific)
+if declare -f "$1" >/dev/null; then
+ # call arguments verbatim
+ "$@"
+else
+ # Show a helpful error
+ echo "'$1' is not a known function name" >&2
+ exit 1
+fi
diff --git a/packages/aws-library/Makefile b/packages/aws-library/Makefile
new file mode 100644
index 00000000000..d7bf9225383
--- /dev/null
+++ b/packages/aws-library/Makefile
@@ -0,0 +1,49 @@
+#
+# Targets for DEVELOPMENT of aws Library
+#
+include ../../scripts/common.Makefile
+include ../../scripts/common-package.Makefile
+
+.PHONY: requirements
+requirements: ## compiles pip requirements (.in -> .txt)
+ @$(MAKE_C) requirements reqs
+
+
+.PHONY: install-dev install-prod install-ci
+install-dev install-prod install-ci: _check_venv_active ## install app in development/production or CI mode
+ # installing in $(subst install-,,$@) mode
+ pip-sync requirements/$(subst install-,,$@).txt
+
+
+.PHONY: tests tests-ci
+tests: ## runs unit tests
+ # running unit tests
+ @pytest \
+ --asyncio-mode=auto \
+ --color=yes \
+ --cov-config=../../.coveragerc \
+ --cov-report=term-missing \
+ --cov=aws_library \
+ --durations=10 \
+ --exitfirst \
+ --failed-first \
+ --pdb \
+ -vv \
+ $(CURDIR)/tests
+
+tests-ci: ## runs unit tests
+ # running unit tests
+ @pytest \
+ --asyncio-mode=auto \
+ --color=yes \
+ --cov-append \
+ --cov-config=../../.coveragerc \
+ --cov-report=term-missing \
+ --cov-report=xml \
+ --cov=aws_library \
+ --durations=10 \
+ --log-date-format="%Y-%m-%d %H:%M:%S" \
+ --log-format="%(asctime)s %(levelname)s %(message)s" \
+ --verbose \
+ -m "not heavy_load" \
+ $(CURDIR)/tests
diff --git a/packages/aws-library/README.md b/packages/aws-library/README.md
new file mode 100644
index 00000000000..c7df3095401
--- /dev/null
+++ b/packages/aws-library/README.md
@@ -0,0 +1,22 @@
+# simcore AWS library
+
+Provides a wrapper around AWS python libraries.
+
+Requirements to be compatible with the library:
+
+- only AWS-related code
+
+
+## Installation
+
+```console
+make help
+make install-dev
+```
+
+## Test
+
+```console
+make help
+make test-dev
+```
diff --git a/packages/aws-library/VERSION b/packages/aws-library/VERSION
new file mode 100644
index 00000000000..6e8bf73aa55
--- /dev/null
+++ b/packages/aws-library/VERSION
@@ -0,0 +1 @@
+0.1.0
diff --git a/packages/aws-library/requirements/Makefile b/packages/aws-library/requirements/Makefile
new file mode 100644
index 00000000000..3f25442b790
--- /dev/null
+++ b/packages/aws-library/requirements/Makefile
@@ -0,0 +1,6 @@
+#
+# Targets to pip-compile requirements
+#
+include ../../../requirements/base.Makefile
+
+# Add here any extra explicit dependency: e.g. _migration.txt: _base.txt
diff --git a/packages/aws-library/requirements/_base.in b/packages/aws-library/requirements/_base.in
new file mode 100644
index 00000000000..a0fd39eb41f
--- /dev/null
+++ b/packages/aws-library/requirements/_base.in
@@ -0,0 +1,12 @@
+#
+# Specifies third-party dependencies for 'aws-library'
+#
+--constraint ../../../requirements/constraints.txt
+--requirement ../../../packages/models-library/requirements/_base.in
+--requirement ../../../packages/service-library/requirements/_base.in
+--requirement ../../../packages/settings-library/requirements/_base.in
+
+aioboto3
+aiocache
+pydantic[email]
+types-aiobotocore[ec2]
diff --git a/packages/aws-library/requirements/_base.txt b/packages/aws-library/requirements/_base.txt
new file mode 100644
index 00000000000..2e125c14ad8
--- /dev/null
+++ b/packages/aws-library/requirements/_base.txt
@@ -0,0 +1,200 @@
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+# pip-compile --output-file=requirements/_base.txt --strip-extras requirements/_base.in
+#
+aio-pika==9.3.0
+ # via -r requirements/../../../packages/service-library/requirements/_base.in
+aioboto3==12.0.0
+ # via -r requirements/_base.in
+aiobotocore==2.7.0
+ # via
+ # aioboto3
+ # aiobotocore
+aiocache==0.12.2
+ # via -r requirements/_base.in
+aiodebug==2.3.0
+ # via -r requirements/../../../packages/service-library/requirements/_base.in
+aiodocker==0.21.0
+ # via -r requirements/../../../packages/service-library/requirements/_base.in
+aiofiles==23.2.1
+ # via -r requirements/../../../packages/service-library/requirements/_base.in
+aiohttp==3.8.6
+ # via
+ # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../requirements/constraints.txt
+ # aiobotocore
+ # aiodocker
+aioitertools==0.11.0
+ # via aiobotocore
+aiormq==6.7.7
+ # via aio-pika
+aiosignal==1.3.1
+ # via aiohttp
+arrow==1.3.0
+ # via
+ # -r requirements/../../../packages/models-library/requirements/_base.in
+ # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
+ # -r requirements/../../../packages/service-library/requirements/_base.in
+async-timeout==4.0.3
+ # via
+ # aiohttp
+ # redis
+attrs==23.1.0
+ # via
+ # aiohttp
+ # jsonschema
+ # referencing
+boto3==1.28.64
+ # via aiobotocore
+botocore==1.31.64
+ # via
+ # aiobotocore
+ # boto3
+ # s3transfer
+botocore-stubs==1.31.85
+ # via types-aiobotocore
+charset-normalizer==3.3.2
+ # via aiohttp
+click==8.1.7
+ # via typer
+dnspython==2.4.2
+ # via email-validator
+email-validator==2.1.0.post1
+ # via pydantic
+frozenlist==1.4.0
+ # via
+ # aiohttp
+ # aiosignal
+idna==3.4
+ # via
+ # email-validator
+ # yarl
+jmespath==1.0.1
+ # via
+ # boto3
+ # botocore
+jsonschema==4.19.2
+ # via
+ # -r requirements/../../../packages/models-library/requirements/_base.in
+ # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
+jsonschema-specifications==2023.7.1
+ # via jsonschema
+markdown-it-py==3.0.0
+ # via rich
+mdurl==0.1.2
+ # via markdown-it-py
+multidict==6.0.4
+ # via
+ # aiohttp
+ # yarl
+orjson==3.9.10
+ # via
+ # -r requirements/../../../packages/models-library/requirements/_base.in
+ # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
+pamqp==3.2.1
+ # via aiormq
+pydantic==1.10.13
+ # via
+ # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../requirements/constraints.txt
+ # -r requirements/../../../packages/models-library/requirements/_base.in
+ # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
+ # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in
+ # -r requirements/../../../packages/service-library/requirements/_base.in
+ # -r requirements/../../../packages/settings-library/requirements/_base.in
+ # -r requirements/_base.in
+pygments==2.16.1
+ # via rich
+pyinstrument==4.6.1
+ # via -r requirements/../../../packages/service-library/requirements/_base.in
+python-dateutil==2.8.2
+ # via
+ # arrow
+ # botocore
+pyyaml==6.0.1
+ # via
+ # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../requirements/constraints.txt
+ # -r requirements/../../../packages/service-library/requirements/_base.in
+redis==5.0.1
+ # via
+ # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../requirements/constraints.txt
+ # -r requirements/../../../packages/service-library/requirements/_base.in
+referencing==0.29.3
+ # via
+ # -c requirements/../../../packages/service-library/requirements/./constraints.txt
+ # jsonschema
+ # jsonschema-specifications
+rich==13.6.0
+ # via
+ # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in
+ # -r requirements/../../../packages/settings-library/requirements/_base.in
+rpds-py==0.12.0
+ # via
+ # jsonschema
+ # referencing
+s3transfer==0.7.0
+ # via boto3
+six==1.16.0
+ # via python-dateutil
+tenacity==8.2.3
+ # via -r requirements/../../../packages/service-library/requirements/_base.in
+toolz==0.12.0
+ # via -r requirements/../../../packages/service-library/requirements/_base.in
+tqdm==4.66.1
+ # via -r requirements/../../../packages/service-library/requirements/_base.in
+typer==0.9.0
+ # via
+ # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in
+ # -r requirements/../../../packages/settings-library/requirements/_base.in
+types-aiobotocore==2.7.0
+ # via -r requirements/_base.in
+types-aiobotocore-ec2==2.7.0
+ # via types-aiobotocore
+types-awscrt==0.19.10
+ # via botocore-stubs
+types-python-dateutil==2.8.19.14
+ # via arrow
+typing-extensions==4.8.0
+ # via
+ # aiodebug
+ # aiodocker
+ # pydantic
+ # typer
+ # types-aiobotocore
+ # types-aiobotocore-ec2
+urllib3==2.0.7
+ # via
+ # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../requirements/constraints.txt
+ # botocore
+wrapt==1.16.0
+ # via aiobotocore
+yarl==1.9.2
+ # via
+ # aio-pika
+ # aiohttp
+ # aiormq
diff --git a/packages/aws-library/requirements/_test.in b/packages/aws-library/requirements/_test.in
new file mode 100644
index 00000000000..75b32719060
--- /dev/null
+++ b/packages/aws-library/requirements/_test.in
@@ -0,0 +1,25 @@
+#
+# Specifies dependencies required to run 'models-library'
+#
+--constraint ../../../requirements/constraints.txt
+
+# Adds base AS CONSTRAINT specs, not requirement.
+# - Resulting _text.txt is a frozen list of EXTRA packages for testing, besides _base.txt
+#
+--constraint _base.txt
+
+# testing
+coverage
+faker
+moto[server]
+pint
+pytest
+pytest-aiohttp # incompatible with pytest-asyncio. See https://github.com/pytest-dev/pytest-asyncio/issues/76
+pytest-cov
+pytest-icdiff
+pytest-instafail
+pytest-mock
+pytest-runner
+pytest-sugar
+python-dotenv
+pyyaml
diff --git a/packages/aws-library/requirements/_test.txt b/packages/aws-library/requirements/_test.txt
new file mode 100644
index 00000000000..a588a55a40a
--- /dev/null
+++ b/packages/aws-library/requirements/_test.txt
@@ -0,0 +1,326 @@
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+# pip-compile --output-file=requirements/_test.txt --strip-extras requirements/_test.in
+#
+aiohttp==3.8.6
+ # via
+ # -c requirements/../../../requirements/constraints.txt
+ # -c requirements/_base.txt
+ # pytest-aiohttp
+aiosignal==1.3.1
+ # via
+ # -c requirements/_base.txt
+ # aiohttp
+async-timeout==4.0.3
+ # via
+ # -c requirements/_base.txt
+ # aiohttp
+attrs==23.1.0
+ # via
+ # -c requirements/_base.txt
+ # aiohttp
+ # jschema-to-python
+ # jsonschema
+ # referencing
+ # sarif-om
+aws-sam-translator==1.79.0
+ # via cfn-lint
+aws-xray-sdk==2.12.1
+ # via moto
+blinker==1.7.0
+ # via flask
+boto3==1.28.64
+ # via
+ # -c requirements/_base.txt
+ # aws-sam-translator
+ # moto
+botocore==1.31.64
+ # via
+ # -c requirements/_base.txt
+ # aws-xray-sdk
+ # boto3
+ # moto
+ # s3transfer
+certifi==2023.7.22
+ # via
+ # -c requirements/../../../requirements/constraints.txt
+ # requests
+cffi==1.16.0
+ # via cryptography
+cfn-lint==0.83.2
+ # via moto
+charset-normalizer==3.3.2
+ # via
+ # -c requirements/_base.txt
+ # aiohttp
+ # requests
+click==8.1.7
+ # via
+ # -c requirements/_base.txt
+ # flask
+coverage==7.3.2
+ # via
+ # -r requirements/_test.in
+ # pytest-cov
+cryptography==41.0.5
+ # via
+ # -c requirements/../../../requirements/constraints.txt
+ # moto
+ # python-jose
+ # sshpubkeys
+docker==6.1.3
+ # via moto
+ecdsa==0.18.0
+ # via
+ # moto
+ # python-jose
+ # sshpubkeys
+exceptiongroup==1.1.3
+ # via pytest
+faker==20.0.0
+ # via -r requirements/_test.in
+flask==3.0.0
+ # via
+ # flask-cors
+ # moto
+flask-cors==4.0.0
+ # via moto
+frozenlist==1.4.0
+ # via
+ # -c requirements/_base.txt
+ # aiohttp
+ # aiosignal
+graphql-core==3.2.3
+ # via moto
+icdiff==2.0.7
+ # via pytest-icdiff
+idna==3.4
+ # via
+ # -c requirements/_base.txt
+ # requests
+ # yarl
+iniconfig==2.0.0
+ # via pytest
+itsdangerous==2.1.2
+ # via flask
+jinja2==3.1.2
+ # via
+ # -c requirements/../../../requirements/constraints.txt
+ # flask
+ # moto
+jmespath==1.0.1
+ # via
+ # -c requirements/_base.txt
+ # boto3
+ # botocore
+jschema-to-python==1.2.3
+ # via cfn-lint
+jsondiff==2.0.0
+ # via moto
+jsonpatch==1.33
+ # via cfn-lint
+jsonpickle==3.0.2
+ # via jschema-to-python
+jsonpointer==2.4
+ # via jsonpatch
+jsonschema==4.19.2
+ # via
+ # -c requirements/_base.txt
+ # aws-sam-translator
+ # cfn-lint
+ # openapi-schema-validator
+ # openapi-spec-validator
+jsonschema-path==0.3.1
+ # via openapi-spec-validator
+jsonschema-specifications==2023.7.1
+ # via
+ # -c requirements/_base.txt
+ # jsonschema
+ # openapi-schema-validator
+junit-xml==1.9
+ # via cfn-lint
+lazy-object-proxy==1.9.0
+ # via openapi-spec-validator
+markupsafe==2.1.3
+ # via
+ # jinja2
+ # werkzeug
+moto==4.2.8
+ # via -r requirements/_test.in
+mpmath==1.3.0
+ # via sympy
+multidict==6.0.4
+ # via
+ # -c requirements/_base.txt
+ # aiohttp
+ # yarl
+networkx==3.2.1
+ # via cfn-lint
+openapi-schema-validator==0.6.2
+ # via openapi-spec-validator
+openapi-spec-validator==0.7.1
+ # via moto
+packaging==23.2
+ # via
+ # docker
+ # pytest
+ # pytest-sugar
+pathable==0.4.3
+ # via jsonschema-path
+pbr==6.0.0
+ # via
+ # jschema-to-python
+ # sarif-om
+pint==0.22
+ # via -r requirements/_test.in
+pluggy==1.3.0
+ # via pytest
+pprintpp==0.4.0
+ # via pytest-icdiff
+py-partiql-parser==0.4.2
+ # via moto
+pyasn1==0.5.0
+ # via
+ # python-jose
+ # rsa
+pycparser==2.21
+ # via cffi
+pydantic==1.10.13
+ # via
+ # -c requirements/../../../requirements/constraints.txt
+ # -c requirements/_base.txt
+ # aws-sam-translator
+pyparsing==3.1.1
+ # via moto
+pytest==7.4.3
+ # via
+ # -r requirements/_test.in
+ # pytest-aiohttp
+ # pytest-asyncio
+ # pytest-cov
+ # pytest-icdiff
+ # pytest-instafail
+ # pytest-mock
+ # pytest-sugar
+pytest-aiohttp==1.0.5
+ # via -r requirements/_test.in
+pytest-asyncio==0.21.1
+ # via pytest-aiohttp
+pytest-cov==4.1.0
+ # via -r requirements/_test.in
+pytest-icdiff==0.8
+ # via -r requirements/_test.in
+pytest-instafail==0.5.0
+ # via -r requirements/_test.in
+pytest-mock==3.12.0
+ # via -r requirements/_test.in
+pytest-runner==6.0.0
+ # via -r requirements/_test.in
+pytest-sugar==0.9.7
+ # via -r requirements/_test.in
+python-dateutil==2.8.2
+ # via
+ # -c requirements/_base.txt
+ # botocore
+ # faker
+ # moto
+python-dotenv==1.0.0
+ # via -r requirements/_test.in
+python-jose==3.3.0
+ # via
+ # moto
+ # python-jose
+pyyaml==6.0.1
+ # via
+ # -c requirements/../../../requirements/constraints.txt
+ # -c requirements/_base.txt
+ # -r requirements/_test.in
+ # cfn-lint
+ # jsonschema-path
+ # moto
+ # responses
+referencing==0.29.3
+ # via
+ # -c requirements/_base.txt
+ # jsonschema
+ # jsonschema-path
+ # jsonschema-specifications
+regex==2023.10.3
+ # via cfn-lint
+requests==2.31.0
+ # via
+ # docker
+ # jsonschema-path
+ # moto
+ # responses
+responses==0.24.0
+ # via moto
+rfc3339-validator==0.1.4
+ # via openapi-schema-validator
+rpds-py==0.12.0
+ # via
+ # -c requirements/_base.txt
+ # jsonschema
+ # referencing
+rsa==4.9
+ # via
+ # -c requirements/../../../requirements/constraints.txt
+ # python-jose
+s3transfer==0.7.0
+ # via
+ # -c requirements/_base.txt
+ # boto3
+sarif-om==1.0.4
+ # via cfn-lint
+six==1.16.0
+ # via
+ # -c requirements/_base.txt
+ # ecdsa
+ # junit-xml
+ # python-dateutil
+ # rfc3339-validator
+sshpubkeys==3.3.1
+ # via moto
+sympy==1.12
+ # via cfn-lint
+termcolor==2.3.0
+ # via pytest-sugar
+tomli==2.0.1
+ # via
+ # coverage
+ # pytest
+typing-extensions==4.8.0
+ # via
+ # -c requirements/_base.txt
+ # aws-sam-translator
+ # pint
+ # pydantic
+urllib3==2.0.7
+ # via
+ # -c requirements/../../../requirements/constraints.txt
+ # -c requirements/_base.txt
+ # botocore
+ # docker
+ # requests
+ # responses
+websocket-client==1.6.4
+ # via docker
+werkzeug==3.0.1
+ # via
+ # flask
+ # moto
+wrapt==1.16.0
+ # via
+ # -c requirements/_base.txt
+ # aws-xray-sdk
+xmltodict==0.13.0
+ # via moto
+yarl==1.9.2
+ # via
+ # -c requirements/_base.txt
+ # aiohttp
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/packages/aws-library/requirements/_tools.in b/packages/aws-library/requirements/_tools.in
new file mode 100644
index 00000000000..1def82c12a3
--- /dev/null
+++ b/packages/aws-library/requirements/_tools.in
@@ -0,0 +1,5 @@
+--constraint ../../../requirements/constraints.txt
+--constraint _base.txt
+--constraint _test.txt
+
+--requirement ../../../requirements/devenv.txt
diff --git a/packages/aws-library/requirements/_tools.txt b/packages/aws-library/requirements/_tools.txt
new file mode 100644
index 00000000000..944a95eebf6
--- /dev/null
+++ b/packages/aws-library/requirements/_tools.txt
@@ -0,0 +1,92 @@
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+# pip-compile --output-file=requirements/_tools.txt --strip-extras requirements/_tools.in
+#
+astroid==3.0.1
+ # via pylint
+black==23.11.0
+ # via -r requirements/../../../requirements/devenv.txt
+build==1.0.3
+ # via pip-tools
+bump2version==1.0.1
+ # via -r requirements/../../../requirements/devenv.txt
+cfgv==3.4.0
+ # via pre-commit
+click==8.1.7
+ # via
+ # -c requirements/_base.txt
+ # -c requirements/_test.txt
+ # black
+ # pip-tools
+dill==0.3.7
+ # via pylint
+distlib==0.3.7
+ # via virtualenv
+filelock==3.13.1
+ # via virtualenv
+identify==2.5.31
+ # via pre-commit
+isort==5.12.0
+ # via
+ # -r requirements/../../../requirements/devenv.txt
+ # pylint
+mccabe==0.7.0
+ # via pylint
+mypy-extensions==1.0.0
+ # via black
+nodeenv==1.8.0
+ # via pre-commit
+packaging==23.2
+ # via
+ # -c requirements/_test.txt
+ # black
+ # build
+pathspec==0.11.2
+ # via black
+pip-tools==7.3.0
+ # via -r requirements/../../../requirements/devenv.txt
+platformdirs==3.11.0
+ # via
+ # black
+ # pylint
+ # virtualenv
+pre-commit==3.5.0
+ # via -r requirements/../../../requirements/devenv.txt
+pylint==3.0.2
+ # via -r requirements/../../../requirements/devenv.txt
+pyproject-hooks==1.0.0
+ # via build
+pyyaml==6.0.1
+ # via
+ # -c requirements/../../../requirements/constraints.txt
+ # -c requirements/_base.txt
+ # -c requirements/_test.txt
+ # pre-commit
+ruff==0.1.5
+ # via -r requirements/../../../requirements/devenv.txt
+tomli==2.0.1
+ # via
+ # -c requirements/_test.txt
+ # black
+ # build
+ # pip-tools
+ # pylint
+ # pyproject-hooks
+tomlkit==0.12.2
+ # via pylint
+typing-extensions==4.8.0
+ # via
+ # -c requirements/_base.txt
+ # -c requirements/_test.txt
+ # astroid
+ # black
+virtualenv==20.24.6
+ # via pre-commit
+wheel==0.41.3
+ # via pip-tools
+
+# The following packages are considered to be unsafe in a requirements file:
+# pip
+# setuptools
diff --git a/packages/aws-library/requirements/ci.txt b/packages/aws-library/requirements/ci.txt
new file mode 100644
index 00000000000..604f51d486e
--- /dev/null
+++ b/packages/aws-library/requirements/ci.txt
@@ -0,0 +1,20 @@
+# Shortcut to install all packages for the contigous integration (CI) of 'models-library'
+#
+# - As ci.txt but w/ tests
+#
+# Usage:
+# pip install -r requirements/ci.txt
+#
+
+# installs base + tests requirements
+--requirement _base.txt
+--requirement _test.txt
+--requirement _tools.txt
+
+# installs this repo's packages
+../pytest-simcore/
+../service-library/
+../settings-library/
+
+# current module
+.
diff --git a/packages/aws-library/requirements/dev.txt b/packages/aws-library/requirements/dev.txt
new file mode 100644
index 00000000000..b183acca92c
--- /dev/null
+++ b/packages/aws-library/requirements/dev.txt
@@ -0,0 +1,20 @@
+# Shortcut to install all packages needed to develop 'models-library'
+#
+# - As ci.txt but with current and repo packages in develop (edit) mode
+#
+# Usage:
+# pip install -r requirements/dev.txt
+#
+
+# installs base + tests requirements
+--requirement _base.txt
+--requirement _test.txt
+--requirement _tools.txt
+
+# installs this repo's packages
+--editable ../pytest-simcore/
+--editable ../service-library/
+--editable ../settings-library/
+
+# current module
+--editable .
diff --git a/packages/aws-library/setup.cfg b/packages/aws-library/setup.cfg
new file mode 100644
index 00000000000..e138d387a7d
--- /dev/null
+++ b/packages/aws-library/setup.cfg
@@ -0,0 +1,17 @@
+[bumpversion]
+current_version = 0.1.0
+commit = True
+message = packages/aws-library version: {current_version} → {new_version}
+tag = False
+commit_args = --no-verify
+
+[bumpversion:file:VERSION]
+
+[bdist_wheel]
+universal = 1
+
+[aliases]
+test = pytest
+
+[tool:pytest]
+asyncio_mode = auto
diff --git a/packages/aws-library/setup.py b/packages/aws-library/setup.py
new file mode 100644
index 00000000000..d4ffa1a620d
--- /dev/null
+++ b/packages/aws-library/setup.py
@@ -0,0 +1,58 @@
+import re
+import sys
+from pathlib import Path
+
+from setuptools import find_packages, setup
+
+
+def read_reqs(reqs_path: Path) -> set[str]:
+ return {
+ r
+ for r in re.findall(
+ r"(^[^#\n-][\w\[,\]]+[-~>=<.\w]*)",
+ reqs_path.read_text(),
+ re.MULTILINE,
+ )
+ if isinstance(r, str)
+ }
+
+
+CURRENT_DIR = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent
+
+INSTALL_REQUIREMENTS = tuple(
+ read_reqs(CURRENT_DIR / "requirements" / "_base.in")
+) # WEAK requirements
+
+TEST_REQUIREMENTS = tuple(
+ read_reqs(CURRENT_DIR / "requirements" / "_test.txt")
+) # STRICT requirements
+
+
+SETUP = {
+ "name": "simcore-aws-library",
+ "version": Path(CURRENT_DIR / "VERSION").read_text().strip(),
+ "author": "Sylvain Anderegg (sanderegg)",
+ "description": "Core service library for AWS APIs",
+ "python_requires": "~=3.10",
+ "classifiers": [
+ "Development Status :: 2 - Pre-Alpha",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Natural Language :: English",
+ "Programming Language :: Python :: 3.10",
+ ],
+ "long_description": Path(CURRENT_DIR / "README.md").read_text(),
+ "license": "MIT license",
+ "install_requires": INSTALL_REQUIREMENTS,
+ "packages": find_packages(where="src"),
+ "package_dir": {"": "src"},
+ "include_package_data": True,
+ "test_suite": "tests",
+ "tests_require": TEST_REQUIREMENTS,
+ "extras_require": {"test": TEST_REQUIREMENTS},
+ "zip_safe": False,
+}
+
+
+if __name__ == "__main__":
+ setup(**SETUP)
diff --git a/packages/aws-library/src/aws_library/__init__.py b/packages/aws-library/src/aws_library/__init__.py
new file mode 100644
index 00000000000..8abb76e0ad2
--- /dev/null
+++ b/packages/aws-library/src/aws_library/__init__.py
@@ -0,0 +1,3 @@
+import pkg_resources
+
+__version__: str = pkg_resources.get_distribution("simcore-aws-library").version
diff --git a/packages/aws-library/src/aws_library/ec2/__init__.py b/packages/aws-library/src/aws_library/ec2/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/aws-library/src/aws_library/ec2/client.py b/packages/aws-library/src/aws_library/ec2/client.py
new file mode 100644
index 00000000000..a88cf93cbd7
--- /dev/null
+++ b/packages/aws-library/src/aws_library/ec2/client.py
@@ -0,0 +1,284 @@
+import contextlib
+import logging
+from dataclasses import dataclass
+from typing import cast
+
+import aioboto3
+import botocore.exceptions
+from aiobotocore.session import ClientCreatorContext
+from aiocache import cached
+from pydantic import ByteSize
+from servicelib.logging_utils import log_context
+from settings_library.ec2 import EC2Settings
+from types_aiobotocore_ec2 import EC2Client
+from types_aiobotocore_ec2.literals import InstanceStateNameType, InstanceTypeType
+from types_aiobotocore_ec2.type_defs import FilterTypeDef
+
+from .errors import (
+ EC2InstanceNotFoundError,
+ EC2InstanceTypeInvalidError,
+ EC2RuntimeError,
+ EC2TooManyInstancesError,
+)
+from .models import (
+ EC2InstanceConfig,
+ EC2InstanceData,
+ EC2InstanceType,
+ EC2Tags,
+ Resources,
+)
+from .utils import compose_user_data
+
+_logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class SimcoreEC2API:
+ client: EC2Client
+ session: aioboto3.Session
+ exit_stack: contextlib.AsyncExitStack
+
+ @classmethod
+ async def create(cls, settings: EC2Settings) -> "SimcoreEC2API":
+ session = aioboto3.Session()
+ session_client = session.client(
+ "ec2",
+ endpoint_url=settings.EC2_ENDPOINT,
+ aws_access_key_id=settings.EC2_ACCESS_KEY_ID,
+ aws_secret_access_key=settings.EC2_SECRET_ACCESS_KEY,
+ region_name=settings.EC2_REGION_NAME,
+ )
+ assert isinstance(session_client, ClientCreatorContext) # nosec
+ exit_stack = contextlib.AsyncExitStack()
+ ec2_client = cast(
+ EC2Client, await exit_stack.enter_async_context(session_client)
+ )
+ return cls(ec2_client, session, exit_stack)
+
+ async def close(self) -> None:
+ await self.exit_stack.aclose()
+
+ async def ping(self) -> bool:
+ try:
+ await self.client.describe_account_attributes()
+ return True
+ except Exception: # pylint: disable=broad-except
+ return False
+
+ @cached(noself=True)
+ async def get_ec2_instance_capabilities(
+ self,
+ instance_type_names: set[InstanceTypeType],
+ ) -> list[EC2InstanceType]:
+ """returns the ec2 instance types from a list of instance type names
+ NOTE: the order might differ!
+ Arguments:
+ instance_type_names -- the types to filter with
+
+ Raises:
+ Ec2InstanceTypeInvalidError: some invalid types were used as filter
+ ClustersKeeperRuntimeError: unexpected error communicating with EC2
+
+ """
+ try:
+ instance_types = await self.client.describe_instance_types(
+ InstanceTypes=list(instance_type_names)
+ )
+ list_instances: list[EC2InstanceType] = []
+ for instance in instance_types.get("InstanceTypes", []):
+ with contextlib.suppress(KeyError):
+ list_instances.append(
+ EC2InstanceType(
+ name=instance["InstanceType"],
+ cpus=instance["VCpuInfo"]["DefaultVCpus"],
+ ram=ByteSize(
+ int(instance["MemoryInfo"]["SizeInMiB"]) * 1024 * 1024
+ ),
+ )
+ )
+ return list_instances
+ except botocore.exceptions.ClientError as exc:
+ if exc.response.get("Error", {}).get("Code", "") == "InvalidInstanceType":
+ raise EC2InstanceTypeInvalidError from exc
+ raise EC2RuntimeError from exc # pragma: no cover
+
+ async def start_aws_instance(
+ self,
+ instance_config: EC2InstanceConfig,
+ number_of_instances: int,
+ max_number_of_instances: int = 10,
+ ) -> list[EC2InstanceData]:
+ with log_context(
+ _logger,
+ logging.INFO,
+ msg=f"launching {number_of_instances} AWS instance(s) {instance_config.type.name} with {instance_config.tags=}",
+ ):
+ # first check the max amount is not already reached
+ current_instances = await self.get_instances(
+ key_names=[instance_config.key_name], tags=instance_config.tags
+ )
+ if len(current_instances) + number_of_instances > max_number_of_instances:
+ raise EC2TooManyInstancesError(num_instances=max_number_of_instances)
+
+ instances = await self.client.run_instances(
+ ImageId=instance_config.ami_id,
+ MinCount=number_of_instances,
+ MaxCount=number_of_instances,
+ InstanceType=instance_config.type.name,
+ InstanceInitiatedShutdownBehavior="terminate",
+ KeyName=instance_config.key_name,
+ TagSpecifications=[
+ {
+ "ResourceType": "instance",
+ "Tags": [
+ {"Key": tag_key, "Value": tag_value}
+ for tag_key, tag_value in instance_config.tags.items()
+ ],
+ }
+ ],
+ UserData=compose_user_data(instance_config.startup_script),
+ NetworkInterfaces=[
+ {
+ "AssociatePublicIpAddress": True,
+ "DeviceIndex": 0,
+ "SubnetId": instance_config.subnet_id,
+ "Groups": instance_config.security_group_ids,
+ }
+ ],
+ )
+ instance_ids = [i["InstanceId"] for i in instances["Instances"]]
+ _logger.info(
+ "New instances launched: %s, waiting for them to start now...",
+ instance_ids,
+ )
+
+ # wait for the instance to be in a pending state
+ # NOTE: reference to EC2 states https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html
+ waiter = self.client.get_waiter("instance_exists")
+ await waiter.wait(InstanceIds=instance_ids)
+ _logger.info("instances %s exists now.", instance_ids)
+
+ # get the private IPs
+ instances = await self.client.describe_instances(InstanceIds=instance_ids)
+ instance_datas = [
+ EC2InstanceData(
+ launch_time=instance["LaunchTime"],
+ id=instance["InstanceId"],
+ aws_private_dns=instance["PrivateDnsName"],
+ aws_public_ip=instance["PublicIpAddress"]
+ if "PublicIpAddress" in instance
+ else None,
+ type=instance["InstanceType"],
+ state=instance["State"]["Name"],
+ tags={tag["Key"]: tag["Value"] for tag in instance["Tags"]},
+ resources=Resources(
+ cpus=instance_config.type.cpus, ram=instance_config.type.ram
+ ),
+ )
+ for instance in instances["Reservations"][0]["Instances"]
+ ]
+ _logger.info(
+ "%s is available, happy computing!!",
+ f"{instance_datas=}",
+ )
+ return instance_datas
+
+ async def get_instances(
+ self,
+ *,
+ key_names: list[str],
+ tags: EC2Tags,
+ state_names: list[InstanceStateNameType] | None = None,
+ ) -> list[EC2InstanceData]:
+ # NOTE: be careful: Name=instance-state-name,Values=["pending", "running"] means pending OR running
+ # NOTE2: AND is done by repeating Name=instance-state-name,Values=pending Name=instance-state-name,Values=running
+ if state_names is None:
+ state_names = ["pending", "running"]
+
+ filters: list[FilterTypeDef] = [
+ {
+ "Name": "key-name",
+ "Values": key_names,
+ },
+ {"Name": "instance-state-name", "Values": state_names},
+ ]
+ filters.extend(
+ [{"Name": f"tag:{key}", "Values": [value]} for key, value in tags.items()]
+ )
+
+ instances = await self.client.describe_instances(Filters=filters)
+ all_instances = []
+ for reservation in instances["Reservations"]:
+ assert "Instances" in reservation # nosec
+ for instance in reservation["Instances"]:
+ assert "LaunchTime" in instance # nosec
+ assert "InstanceId" in instance # nosec
+ assert "PrivateDnsName" in instance # nosec
+ assert "InstanceType" in instance # nosec
+ assert "State" in instance # nosec
+ assert "Name" in instance["State"] # nosec
+ ec2_instance_types = await self.get_ec2_instance_capabilities(
+ {instance["InstanceType"]}
+ )
+ assert len(ec2_instance_types) == 1 # nosec
+ assert "Tags" in instance # nosec
+ all_instances.append(
+ EC2InstanceData(
+ launch_time=instance["LaunchTime"],
+ id=instance["InstanceId"],
+ aws_private_dns=instance["PrivateDnsName"],
+ aws_public_ip=instance["PublicIpAddress"]
+ if "PublicIpAddress" in instance
+ else None,
+ type=instance["InstanceType"],
+ state=instance["State"]["Name"],
+ resources=Resources(
+ cpus=ec2_instance_types[0].cpus,
+ ram=ec2_instance_types[0].ram,
+ ),
+ tags={tag["Key"]: tag["Value"] for tag in instance["Tags"]},
+ )
+ )
+ _logger.debug(
+ "received: %s instances with %s", f"{len(all_instances)}", f"{state_names=}"
+ )
+ return all_instances
+
+ async def terminate_instances(self, instance_datas: list[EC2InstanceData]) -> None:
+ try:
+ with log_context(
+ _logger,
+ logging.INFO,
+ msg=f"terminating instances {[i.id for i in instance_datas]}",
+ ):
+ await self.client.terminate_instances(
+ InstanceIds=[i.id for i in instance_datas]
+ )
+ except botocore.exceptions.ClientError as exc:
+ if (
+ exc.response.get("Error", {}).get("Code", "")
+ == "InvalidInstanceID.NotFound"
+ ):
+ raise EC2InstanceNotFoundError from exc
+ raise # pragma: no cover
+
+ async def set_instances_tags(
+ self, instances: list[EC2InstanceData], *, tags: EC2Tags
+ ) -> None:
+ try:
+ with log_context(
+ _logger,
+ logging.DEBUG,
+ msg=f"setting {tags=} on instances '[{[i.id for i in instances]}]'",
+ ):
+ await self.client.create_tags(
+ Resources=[i.id for i in instances],
+ Tags=[
+ {"Key": tag_key, "Value": tag_value}
+ for tag_key, tag_value in tags.items()
+ ],
+ )
+ except botocore.exceptions.ClientError as exc:
+ if exc.response.get("Error", {}).get("Code", "") == "InvalidID":
+ raise EC2InstanceNotFoundError from exc
+ raise # pragma: no cover
diff --git a/packages/aws-library/src/aws_library/ec2/errors.py b/packages/aws-library/src/aws_library/ec2/errors.py
new file mode 100644
index 00000000000..2253fd17680
--- /dev/null
+++ b/packages/aws-library/src/aws_library/ec2/errors.py
@@ -0,0 +1,19 @@
+from pydantic.errors import PydanticErrorMixin
+
+
+class EC2RuntimeError(PydanticErrorMixin, RuntimeError):
+ msg_template: str = "EC2 client unexpected error"
+
+
+class EC2InstanceNotFoundError(EC2RuntimeError):
+ msg_template: str = "EC2 instance was not found"
+
+
+class EC2InstanceTypeInvalidError(EC2RuntimeError):
+ msg_template: str = "EC2 instance type invalid"
+
+
+class EC2TooManyInstancesError(EC2RuntimeError):
+ msg_template: str = (
+ "The maximum amount of instances {num_instances} is already reached!"
+ )
diff --git a/packages/aws-library/src/aws_library/ec2/models.py b/packages/aws-library/src/aws_library/ec2/models.py
new file mode 100644
index 00000000000..d124763cebd
--- /dev/null
+++ b/packages/aws-library/src/aws_library/ec2/models.py
@@ -0,0 +1,76 @@
+import datetime
+from dataclasses import dataclass
+from typing import TypeAlias
+
+from pydantic import BaseModel, ByteSize, NonNegativeFloat, PositiveInt
+from types_aiobotocore_ec2.literals import InstanceStateNameType, InstanceTypeType
+
+
+class Resources(BaseModel):
+ cpus: NonNegativeFloat
+ ram: ByteSize
+
+ @classmethod
+ def create_as_empty(cls) -> "Resources":
+ return cls(cpus=0, ram=ByteSize(0))
+
+ def __ge__(self, other: "Resources") -> bool:
+ return self.cpus >= other.cpus and self.ram >= other.ram
+
+ def __gt__(self, other: "Resources") -> bool:
+ return self.cpus > other.cpus or self.ram > other.ram
+
+ def __add__(self, other: "Resources") -> "Resources":
+ return Resources.construct(
+ **{
+ key: a + b
+ for (key, a), b in zip(
+ self.dict().items(), other.dict().values(), strict=True
+ )
+ }
+ )
+
+ def __sub__(self, other: "Resources") -> "Resources":
+ return Resources.construct(
+ **{
+ key: a - b
+ for (key, a), b in zip(
+ self.dict().items(), other.dict().values(), strict=True
+ )
+ }
+ )
+
+
+@dataclass(frozen=True)
+class EC2InstanceType:
+ name: InstanceTypeType
+ cpus: PositiveInt
+ ram: ByteSize
+
+
+InstancePrivateDNSName: TypeAlias = str
+EC2Tags: TypeAlias = dict[str, str]
+
+
+@dataclass(frozen=True)
+class EC2InstanceData:
+ launch_time: datetime.datetime
+ id: str # noqa: A003
+ aws_private_dns: InstancePrivateDNSName
+ aws_public_ip: str | None
+ type: InstanceTypeType # noqa: A003
+ state: InstanceStateNameType
+ resources: Resources
+ tags: EC2Tags
+
+
+@dataclass(frozen=True)
+class EC2InstanceConfig:
+ type: EC2InstanceType
+ tags: EC2Tags
+ startup_script: str
+
+ ami_id: str
+ key_name: str
+ security_group_ids: list[str]
+ subnet_id: str
diff --git a/packages/aws-library/src/aws_library/ec2/utils.py b/packages/aws-library/src/aws_library/ec2/utils.py
new file mode 100644
index 00000000000..43e27fe1b6d
--- /dev/null
+++ b/packages/aws-library/src/aws_library/ec2/utils.py
@@ -0,0 +1,10 @@
+from textwrap import dedent
+
+
+def compose_user_data(docker_join_bash_command: str) -> str:
+ return dedent(
+ f"""\
+#!/bin/bash
+{docker_join_bash_command}
+"""
+ )
diff --git a/packages/aws-library/tests/conftest.py b/packages/aws-library/tests/conftest.py
new file mode 100644
index 00000000000..9d04f458812
--- /dev/null
+++ b/packages/aws-library/tests/conftest.py
@@ -0,0 +1,23 @@
+# pylint: disable=redefined-outer-name
+# pylint: disable=unused-argument
+# pylint: disable=unused-import
+from pathlib import Path
+
+import aws_library
+import pytest
+
+pytest_plugins = [
+ "pytest_simcore.aws_server",
+ "pytest_simcore.aws_ec2_service",
+ "pytest_simcore.environment_configs",
+ "pytest_simcore.repository_paths",
+ "pytest_simcore.pydantic_models",
+ "pytest_simcore.pytest_global_environs",
+]
+
+
+@pytest.fixture(scope="session")
+def package_dir() -> Path:
+ pdir = Path(aws_library.__file__).resolve().parent
+ assert pdir.exists()
+ return pdir
diff --git a/packages/aws-library/tests/test_ec2_client.py b/packages/aws-library/tests/test_ec2_client.py
new file mode 100644
index 00000000000..1e85be088d1
--- /dev/null
+++ b/packages/aws-library/tests/test_ec2_client.py
@@ -0,0 +1,430 @@
+# pylint:disable=unused-variable
+# pylint:disable=unused-argument
+# pylint:disable=redefined-outer-name
+
+
+from collections.abc import AsyncIterator, Callable
+from typing import cast, get_args
+
+import botocore.exceptions
+import pytest
+from aws_library.ec2.client import SimcoreEC2API
+from aws_library.ec2.errors import (
+ EC2InstanceNotFoundError,
+ EC2InstanceTypeInvalidError,
+ EC2TooManyInstancesError,
+)
+from aws_library.ec2.models import (
+ EC2InstanceConfig,
+ EC2InstanceData,
+ EC2InstanceType,
+ EC2Tags,
+)
+from faker import Faker
+from moto.server import ThreadedMotoServer
+from settings_library.ec2 import EC2Settings
+from types_aiobotocore_ec2 import EC2Client
+from types_aiobotocore_ec2.literals import InstanceStateNameType, InstanceTypeType
+
+
+def _ec2_allowed_types() -> list[InstanceTypeType]:
+ return ["t2.nano", "m5.12xlarge", "g4dn.4xlarge"]
+
+
+@pytest.fixture(scope="session")
+def ec2_allowed_instances() -> list[InstanceTypeType]:
+ return _ec2_allowed_types()
+
+
+@pytest.fixture
+async def simcore_ec2_api(
+ mocked_ec2_server_settings: EC2Settings,
+) -> AsyncIterator[SimcoreEC2API]:
+ ec2 = await SimcoreEC2API.create(settings=mocked_ec2_server_settings)
+ assert ec2
+ assert ec2.client
+ assert ec2.exit_stack
+ assert ec2.session
+ yield ec2
+ await ec2.close()
+
+
+async def test_ec2_client_lifespan(simcore_ec2_api: SimcoreEC2API):
+ ...
+
+
+async def test_aiobotocore_ec2_client_when_ec2_server_goes_up_and_down(
+ mocked_aws_server: ThreadedMotoServer,
+ ec2_client: EC2Client,
+):
+ # passes without exception
+ await ec2_client.describe_account_attributes(DryRun=True)
+ mocked_aws_server.stop()
+ with pytest.raises(botocore.exceptions.EndpointConnectionError):
+ await ec2_client.describe_account_attributes(DryRun=True)
+
+ # restart
+ mocked_aws_server.start()
+ # passes without exception
+ await ec2_client.describe_account_attributes(DryRun=True)
+
+
+async def test_ping(
+ mocked_aws_server: ThreadedMotoServer,
+ simcore_ec2_api: SimcoreEC2API,
+):
+ assert await simcore_ec2_api.ping() is True
+ mocked_aws_server.stop()
+ assert await simcore_ec2_api.ping() is False
+ mocked_aws_server.start()
+ assert await simcore_ec2_api.ping() is True
+
+
+@pytest.fixture
+def ec2_instance_config(
+ fake_ec2_instance_type: EC2InstanceType,
+ faker: Faker,
+ aws_subnet_id: str,
+ aws_security_group_id: str,
+ aws_ami_id: str,
+) -> EC2InstanceConfig:
+ return EC2InstanceConfig(
+ type=fake_ec2_instance_type,
+ tags=faker.pydict(allowed_types=(str,)),
+ startup_script=faker.pystr(),
+ ami_id=aws_ami_id,
+ key_name=faker.pystr(),
+ security_group_ids=[aws_security_group_id],
+ subnet_id=aws_subnet_id,
+ )
+
+
+async def test_get_ec2_instance_capabilities(
+ simcore_ec2_api: SimcoreEC2API,
+ ec2_allowed_instances: list[InstanceTypeType],
+):
+ instance_types: list[
+ EC2InstanceType
+ ] = await simcore_ec2_api.get_ec2_instance_capabilities(
+ cast(
+ set[InstanceTypeType],
+ set(ec2_allowed_instances),
+ )
+ )
+ assert instance_types
+ assert len(instance_types) == len(ec2_allowed_instances)
+
+ # all the instance names are found and valid
+ assert all(i.name in ec2_allowed_instances for i in instance_types)
+ for instance_type_name in ec2_allowed_instances:
+ assert any(i.name == instance_type_name for i in instance_types)
+
+
+async def test_get_ec2_instance_capabilities_empty_list_returns_all_options(
+ simcore_ec2_api: SimcoreEC2API,
+):
+ instance_types = await simcore_ec2_api.get_ec2_instance_capabilities(set())
+ assert instance_types
+ # NOTE: this might need adaptation when moto is updated
+ assert 700 < len(instance_types) < 800
+
+
+async def test_get_ec2_instance_capabilities_with_invalid_type_raises(
+ simcore_ec2_api: SimcoreEC2API,
+ faker: Faker,
+):
+ with pytest.raises(EC2InstanceTypeInvalidError):
+ await simcore_ec2_api.get_ec2_instance_capabilities(set(faker.pystr()))
+
+
+@pytest.fixture(params=_ec2_allowed_types())
+async def fake_ec2_instance_type(
+ simcore_ec2_api: SimcoreEC2API,
+ request: pytest.FixtureRequest,
+) -> EC2InstanceType:
+ instance_type_name: InstanceTypeType = request.param
+ instance_types: list[
+ EC2InstanceType
+ ] = await simcore_ec2_api.get_ec2_instance_capabilities({instance_type_name})
+
+ assert len(instance_types) == 1
+ return instance_types[0]
+
+
+async def _assert_no_instances_in_ec2(ec2_client: EC2Client) -> None:
+ all_instances = await ec2_client.describe_instances()
+ assert not all_instances["Reservations"]
+
+
+async def _assert_instances_in_ec2(
+ ec2_client: EC2Client,
+ *,
+ expected_num_reservations: int,
+ expected_num_instances: int,
+ expected_instance_type: EC2InstanceType,
+ expected_tags: EC2Tags,
+ expected_state: str,
+) -> None:
+ all_instances = await ec2_client.describe_instances()
+ assert len(all_instances["Reservations"]) == expected_num_reservations
+ for reservation in all_instances["Reservations"]:
+ assert "Instances" in reservation
+ assert len(reservation["Instances"]) == expected_num_instances
+ for instance in reservation["Instances"]:
+ assert "InstanceType" in instance
+ assert instance["InstanceType"] == expected_instance_type.name
+ assert "Tags" in instance
+ assert instance["Tags"] == [
+ {"Key": key, "Value": value} for key, value in expected_tags.items()
+ ]
+ assert "State" in instance
+ assert "Name" in instance["State"]
+ assert instance["State"]["Name"] == expected_state
+
+
+async def test_start_aws_instance(
+ simcore_ec2_api: SimcoreEC2API,
+ ec2_client: EC2Client,
+ ec2_instance_config: EC2InstanceConfig,
+):
+ await _assert_no_instances_in_ec2(ec2_client)
+
+ number_of_instances = 1
+
+ # let's create a first reservation and check that it is correctly created in EC2
+ await simcore_ec2_api.start_aws_instance(
+ ec2_instance_config, number_of_instances=number_of_instances
+ )
+ await _assert_instances_in_ec2(
+ ec2_client,
+ expected_num_reservations=1,
+ expected_num_instances=number_of_instances,
+ expected_instance_type=ec2_instance_config.type,
+ expected_tags=ec2_instance_config.tags,
+ expected_state="running",
+ )
+
+ # create a second reservation
+ await simcore_ec2_api.start_aws_instance(
+ ec2_instance_config, number_of_instances=number_of_instances
+ )
+ await _assert_instances_in_ec2(
+ ec2_client,
+ expected_num_reservations=2,
+ expected_num_instances=number_of_instances,
+ expected_instance_type=ec2_instance_config.type,
+ expected_tags=ec2_instance_config.tags,
+ expected_state="running",
+ )
+
+
+@pytest.mark.parametrize("max_num_instances", [13])
+async def test_start_aws_instance_is_limited_in_number_of_instances(
+ simcore_ec2_api: SimcoreEC2API,
+ ec2_client: EC2Client,
+ ec2_instance_config: EC2InstanceConfig,
+ max_num_instances: int,
+):
+ await _assert_no_instances_in_ec2(ec2_client)
+
+ # create many instances in one go shall fail
+ with pytest.raises(EC2TooManyInstancesError):
+ await simcore_ec2_api.start_aws_instance(
+ ec2_instance_config,
+ number_of_instances=max_num_instances + 1,
+ max_number_of_instances=max_num_instances,
+ )
+ await _assert_no_instances_in_ec2(ec2_client)
+
+ # create instances 1 by 1
+ for _ in range(max_num_instances):
+ await simcore_ec2_api.start_aws_instance(
+ ec2_instance_config,
+ number_of_instances=1,
+ max_number_of_instances=max_num_instances,
+ )
+ await _assert_instances_in_ec2(
+ ec2_client,
+ expected_num_reservations=max_num_instances,
+ expected_num_instances=1,
+ expected_instance_type=ec2_instance_config.type,
+ expected_tags=ec2_instance_config.tags,
+ expected_state="running",
+ )
+
+ # now creating one more shall fail
+ with pytest.raises(EC2TooManyInstancesError):
+ await simcore_ec2_api.start_aws_instance(
+ ec2_instance_config,
+ number_of_instances=1,
+ max_number_of_instances=max_num_instances,
+ )
+ await _assert_instances_in_ec2(
+ ec2_client,
+ expected_num_reservations=max_num_instances,
+ expected_num_instances=1,
+ expected_instance_type=ec2_instance_config.type,
+ expected_tags=ec2_instance_config.tags,
+ expected_state="running",
+ )
+
+
+async def test_get_instances(
+ simcore_ec2_api: SimcoreEC2API,
+ ec2_client: EC2Client,
+ faker: Faker,
+ ec2_instance_config: EC2InstanceConfig,
+):
+ # we have nothing running now in ec2
+ await _assert_no_instances_in_ec2(ec2_client)
+ assert (
+ await simcore_ec2_api.get_instances(
+ key_names=[ec2_instance_config.key_name], tags={}
+ )
+ == []
+ )
+
+ # create some instance
+ _MAX_NUM_INSTANCES = 10
+ num_instances = faker.pyint(min_value=1, max_value=_MAX_NUM_INSTANCES)
+ created_instances = await simcore_ec2_api.start_aws_instance(
+ ec2_instance_config, number_of_instances=num_instances
+ )
+ await _assert_instances_in_ec2(
+ ec2_client,
+ expected_num_reservations=1,
+ expected_num_instances=num_instances,
+ expected_instance_type=ec2_instance_config.type,
+ expected_tags=ec2_instance_config.tags,
+ expected_state="running",
+ )
+ # this returns all the entries using thes key names
+ instance_received = await simcore_ec2_api.get_instances(
+ key_names=[ec2_instance_config.key_name], tags={}
+ )
+ assert created_instances == instance_received
+
+ # passing the tags will return the same
+ instance_received = await simcore_ec2_api.get_instances(
+ key_names=[ec2_instance_config.key_name], tags=ec2_instance_config.tags
+ )
+ assert created_instances == instance_received
+
+ # asking for running state will also return the same
+ instance_received = await simcore_ec2_api.get_instances(
+ key_names=[ec2_instance_config.key_name],
+ tags=ec2_instance_config.tags,
+ state_names=["running"],
+ )
+ assert created_instances == instance_received
+
+ # asking for other states shall return nothing
+ for state in get_args(InstanceStateNameType):
+ instance_received = await simcore_ec2_api.get_instances(
+ key_names=[ec2_instance_config.key_name],
+ tags=ec2_instance_config.tags,
+ state_names=[state],
+ )
+ if state == "running":
+ assert created_instances == instance_received
+ else:
+ assert not instance_received
+
+
+async def test_terminate_instance(
+ simcore_ec2_api: SimcoreEC2API,
+ ec2_client: EC2Client,
+ faker: Faker,
+ ec2_instance_config: EC2InstanceConfig,
+):
+ # we have nothing running now in ec2
+ await _assert_no_instances_in_ec2(ec2_client)
+ # create some instance
+ _NUM_INSTANCES = 10
+ num_instances = faker.pyint(min_value=1, max_value=_NUM_INSTANCES)
+ created_instances = await simcore_ec2_api.start_aws_instance(
+ ec2_instance_config, number_of_instances=num_instances
+ )
+ await _assert_instances_in_ec2(
+ ec2_client,
+ expected_num_reservations=1,
+ expected_num_instances=num_instances,
+ expected_instance_type=ec2_instance_config.type,
+ expected_tags=ec2_instance_config.tags,
+ expected_state="running",
+ )
+
+ # terminate the instance
+ await simcore_ec2_api.terminate_instances(created_instances)
+ await _assert_instances_in_ec2(
+ ec2_client,
+ expected_num_reservations=1,
+ expected_num_instances=num_instances,
+ expected_instance_type=ec2_instance_config.type,
+ expected_tags=ec2_instance_config.tags,
+ expected_state="terminated",
+ )
+ # calling it several times is ok, the instance stays a while
+ await simcore_ec2_api.terminate_instances(created_instances)
+ await _assert_instances_in_ec2(
+ ec2_client,
+ expected_num_reservations=1,
+ expected_num_instances=num_instances,
+ expected_instance_type=ec2_instance_config.type,
+ expected_tags=ec2_instance_config.tags,
+ expected_state="terminated",
+ )
+
+
+async def test_terminate_instance_not_existing_raises(
+ simcore_ec2_api: SimcoreEC2API,
+ ec2_client: EC2Client,
+ fake_ec2_instance_data: Callable[..., EC2InstanceData],
+):
+ await _assert_no_instances_in_ec2(ec2_client)
+ with pytest.raises(EC2InstanceNotFoundError):
+ await simcore_ec2_api.terminate_instances([fake_ec2_instance_data()])
+
+
+async def test_set_instance_tags(
+ simcore_ec2_api: SimcoreEC2API,
+ ec2_client: EC2Client,
+ faker: Faker,
+ ec2_instance_config: EC2InstanceConfig,
+):
+ await _assert_no_instances_in_ec2(ec2_client)
+ # create some instance
+ _MAX_NUM_INSTANCES = 10
+ num_instances = faker.pyint(min_value=1, max_value=_MAX_NUM_INSTANCES)
+ created_instances = await simcore_ec2_api.start_aws_instance(
+ ec2_instance_config, number_of_instances=num_instances
+ )
+ await _assert_instances_in_ec2(
+ ec2_client,
+ expected_num_reservations=1,
+ expected_num_instances=num_instances,
+ expected_instance_type=ec2_instance_config.type,
+ expected_tags=ec2_instance_config.tags,
+ expected_state="running",
+ )
+
+ new_tags = faker.pydict(allowed_types=(str,))
+ await simcore_ec2_api.set_instances_tags(created_instances, tags=new_tags)
+ await _assert_instances_in_ec2(
+ ec2_client,
+ expected_num_reservations=1,
+ expected_num_instances=num_instances,
+ expected_instance_type=ec2_instance_config.type,
+ expected_tags=ec2_instance_config.tags | new_tags,
+ expected_state="running",
+ )
+
+
+async def test_set_instance_tags_not_existing_raises(
+ simcore_ec2_api: SimcoreEC2API,
+ ec2_client: EC2Client,
+ fake_ec2_instance_data: Callable[..., EC2InstanceData],
+):
+ await _assert_no_instances_in_ec2(ec2_client)
+ with pytest.raises(EC2InstanceNotFoundError):
+ await simcore_ec2_api.set_instances_tags([fake_ec2_instance_data()], tags={})
diff --git a/packages/aws-library/tests/test_ec2_models.py b/packages/aws-library/tests/test_ec2_models.py
new file mode 100644
index 00000000000..aac4a1d7863
--- /dev/null
+++ b/packages/aws-library/tests/test_ec2_models.py
@@ -0,0 +1,124 @@
+# pylint: disable=redefined-outer-name
+# pylint: disable=unused-argument
+# pylint: disable=unused-variable
+
+
+import pytest
+from aws_library.ec2.models import Resources
+from pydantic import ByteSize
+
+
+@pytest.mark.parametrize(
+ "a,b,a_greater_or_equal_than_b",
+ [
+ (
+ Resources(cpus=0.2, ram=ByteSize(0)),
+ Resources(cpus=0.1, ram=ByteSize(0)),
+ True,
+ ),
+ (
+ Resources(cpus=0.1, ram=ByteSize(0)),
+ Resources(cpus=0.1, ram=ByteSize(0)),
+ True,
+ ),
+ (
+ Resources(cpus=0.1, ram=ByteSize(1)),
+ Resources(cpus=0.1, ram=ByteSize(0)),
+ True,
+ ),
+ (
+ Resources(cpus=0.05, ram=ByteSize(1)),
+ Resources(cpus=0.1, ram=ByteSize(0)),
+ False,
+ ),
+ (
+ Resources(cpus=0.1, ram=ByteSize(0)),
+ Resources(cpus=0.1, ram=ByteSize(1)),
+ False,
+ ),
+ ],
+)
+def test_resources_ge_operator(
+ a: Resources, b: Resources, a_greater_or_equal_than_b: bool
+):
+ assert (a >= b) is a_greater_or_equal_than_b
+
+
+@pytest.mark.parametrize(
+ "a,b,a_greater_than_b",
+ [
+ (
+ Resources(cpus=0.2, ram=ByteSize(0)),
+ Resources(cpus=0.1, ram=ByteSize(0)),
+ True,
+ ),
+ (
+ Resources(cpus=0.1, ram=ByteSize(0)),
+ Resources(cpus=0.1, ram=ByteSize(0)),
+ False,
+ ),
+ (
+ Resources(cpus=0.1, ram=ByteSize(1)),
+ Resources(cpus=0.1, ram=ByteSize(0)),
+ True,
+ ),
+ (
+ Resources(cpus=0.05, ram=ByteSize(1)),
+ Resources(cpus=0.1, ram=ByteSize(0)),
+ True,
+ ),
+ (
+ Resources(cpus=0.1, ram=ByteSize(0)),
+ Resources(cpus=0.1, ram=ByteSize(1)),
+ False,
+ ),
+ ],
+)
+def test_resources_gt_operator(a: Resources, b: Resources, a_greater_than_b: bool):
+ assert (a > b) is a_greater_than_b
+
+
+@pytest.mark.parametrize(
+ "a,b,result",
+ [
+ (
+ Resources(cpus=0, ram=ByteSize(0)),
+ Resources(cpus=1, ram=ByteSize(34)),
+ Resources(cpus=1, ram=ByteSize(34)),
+ ),
+ (
+ Resources(cpus=0.1, ram=ByteSize(-1)),
+ Resources(cpus=1, ram=ByteSize(34)),
+ Resources(cpus=1.1, ram=ByteSize(33)),
+ ),
+ ],
+)
+def test_resources_add(a: Resources, b: Resources, result: Resources):
+ assert a + b == result
+ a += b
+ assert a == result
+
+
+def test_resources_create_as_empty():
+ assert Resources.create_as_empty() == Resources(cpus=0, ram=ByteSize(0))
+
+
+@pytest.mark.parametrize(
+ "a,b,result",
+ [
+ (
+ Resources(cpus=0, ram=ByteSize(0)),
+ Resources(cpus=1, ram=ByteSize(34)),
+ Resources.construct(cpus=-1, ram=ByteSize(-34)),
+ ),
+ (
+ Resources(cpus=0.1, ram=ByteSize(-1)),
+ Resources(cpus=1, ram=ByteSize(34)),
+ Resources.construct(cpus=-0.9, ram=ByteSize(-35)),
+ ),
+ ],
+)
+def test_resources_sub(a: Resources, b: Resources, result: Resources):
+ assert a - b == result
+ a -= b
+ assert a == result
diff --git a/packages/aws-library/tests/test_ec2_utils.py b/packages/aws-library/tests/test_ec2_utils.py
new file mode 100644
index 00000000000..ce03ef066b9
--- /dev/null
+++ b/packages/aws-library/tests/test_ec2_utils.py
@@ -0,0 +1,7 @@
+from aws_library.ec2.utils import compose_user_data
+from faker import Faker
+
+
+def test_compose_user_data(faker: Faker):
+ assert compose_user_data(faker.pystr()).startswith("#!/bin/bash\n")
+ assert compose_user_data(faker.pystr()).endswith("\n")
diff --git a/packages/models-library/src/models_library/api_schemas_clusters_keeper/ec2_instances.py b/packages/models-library/src/models_library/api_schemas_clusters_keeper/ec2_instances.py
index 373a12af823..16f47d2a3fd 100644
--- a/packages/models-library/src/models_library/api_schemas_clusters_keeper/ec2_instances.py
+++ b/packages/models-library/src/models_library/api_schemas_clusters_keeper/ec2_instances.py
@@ -4,7 +4,7 @@
@dataclass(frozen=True)
-class EC2InstanceType:
+class EC2InstanceTypeGet:
name: str
cpus: PositiveInt
ram: ByteSize
diff --git a/packages/pytest-simcore/setup.py b/packages/pytest-simcore/setup.py
index 61874d47ea8..b9ee764e695 100644
--- a/packages/pytest-simcore/setup.py
+++ b/packages/pytest-simcore/setup.py
@@ -25,6 +25,7 @@
"aiohttp",
"aioredis",
"docker",
+ "moto[server]",
"python-socketio",
"PyYAML",
"sqlalchemy[postgresql_psycopg2binary]",
diff --git a/packages/pytest-simcore/src/pytest_simcore/aioresponses_mocker.py b/packages/pytest-simcore/src/pytest_simcore/aioresponses_mocker.py
index 11beaa7e325..c91edc673cb 100644
--- a/packages/pytest-simcore/src/pytest_simcore/aioresponses_mocker.py
+++ b/packages/pytest-simcore/src/pytest_simcore/aioresponses_mocker.py
@@ -1,7 +1,7 @@
import pytest
from aioresponses import aioresponses as AioResponsesMock
-from .helpers.utils_docker import get_localhost_ip
+from .helpers.utils_host import get_localhost_ip
# WARNING: any request done through the client will go through aioresponses. It is
# unfortunate but that means any valid request (like calling the test server) prefix must be set as passthrough.
diff --git a/packages/pytest-simcore/src/pytest_simcore/aws_ec2_service.py b/packages/pytest-simcore/src/pytest_simcore/aws_ec2_service.py
new file mode 100644
index 00000000000..641664a5292
--- /dev/null
+++ b/packages/pytest-simcore/src/pytest_simcore/aws_ec2_service.py
@@ -0,0 +1,149 @@
+# pylint: disable=redefined-outer-name
+# pylint: disable=unused-argument
+# pylint: disable=unused-import
+
+import contextlib
+import datetime
+import random
+from collections.abc import AsyncIterator, Callable
+from typing import cast
+
+import aioboto3
+import pytest
+from aiobotocore.session import ClientCreatorContext
+from aws_library.ec2.models import EC2InstanceData, Resources
+from faker import Faker
+from pydantic import ByteSize
+from settings_library.ec2 import EC2Settings
+from types_aiobotocore_ec2.client import EC2Client
+
+
+@pytest.fixture
+async def ec2_client(
+ mocked_ec2_server_settings: EC2Settings,
+) -> AsyncIterator[EC2Client]:
+ session = aioboto3.Session()
+ exit_stack = contextlib.AsyncExitStack()
+ session_client = session.client(
+ "ec2",
+ endpoint_url=mocked_ec2_server_settings.EC2_ENDPOINT,
+ aws_access_key_id=mocked_ec2_server_settings.EC2_ACCESS_KEY_ID,
+ aws_secret_access_key=mocked_ec2_server_settings.EC2_SECRET_ACCESS_KEY,
+ region_name=mocked_ec2_server_settings.EC2_REGION_NAME,
+ )
+ assert isinstance(session_client, ClientCreatorContext)
+ ec2_client = cast(EC2Client, await exit_stack.enter_async_context(session_client))
+
+ yield ec2_client
+
+ await exit_stack.aclose()
+
+
+@pytest.fixture(scope="session")
+def vpc_cidr_block() -> str:
+ return "10.0.0.0/16"
+
+
+@pytest.fixture
+async def aws_vpc_id(
+ ec2_client: EC2Client,
+ vpc_cidr_block: str,
+) -> AsyncIterator[str]:
+ vpc = await ec2_client.create_vpc(
+ CidrBlock=vpc_cidr_block,
+ )
+ vpc_id = vpc["Vpc"]["VpcId"] # type: ignore
+ print(f"--> Created Vpc in AWS with {vpc_id=}")
+ yield vpc_id
+
+ await ec2_client.delete_vpc(VpcId=vpc_id)
+ print(f"<-- Deleted Vpc in AWS with {vpc_id=}")
+
+
+@pytest.fixture(scope="session")
+def subnet_cidr_block() -> str:
+ return "10.0.1.0/24"
+
+
+@pytest.fixture
+async def aws_subnet_id(
+ aws_vpc_id: str,
+ ec2_client: EC2Client,
+ subnet_cidr_block: str,
+) -> AsyncIterator[str]:
+ subnet = await ec2_client.create_subnet(
+ CidrBlock=subnet_cidr_block, VpcId=aws_vpc_id
+ )
+ assert "Subnet" in subnet
+ assert "SubnetId" in subnet["Subnet"]
+ subnet_id = subnet["Subnet"]["SubnetId"]
+ print(f"--> Created Subnet in AWS with {subnet_id=}")
+
+ yield subnet_id
+
+ # all the instances in the subnet must be terminated before that works
+ instances_in_subnet = await ec2_client.describe_instances(
+ Filters=[{"Name": "subnet-id", "Values": [subnet_id]}]
+ )
+ if instances_in_subnet["Reservations"]:
+ print(f"--> terminating {len(instances_in_subnet)} instances in subnet")
+ await ec2_client.terminate_instances(
+ InstanceIds=[
+ instance["Instances"][0]["InstanceId"] # type: ignore
+ for instance in instances_in_subnet["Reservations"]
+ ]
+ )
+ print(f"<-- terminated {len(instances_in_subnet)} instances in subnet")
+
+ await ec2_client.delete_subnet(SubnetId=subnet_id)
+ subnets = await ec2_client.describe_subnets()
+ print(f"<-- Deleted Subnet in AWS with {subnet_id=}")
+ print(f"current {subnets=}")
+
+
+@pytest.fixture
+async def aws_security_group_id(
+ faker: Faker,
+ aws_vpc_id: str,
+ ec2_client: EC2Client,
+) -> AsyncIterator[str]:
+ security_group = await ec2_client.create_security_group(
+ Description=faker.text(), GroupName=faker.pystr(), VpcId=aws_vpc_id
+ )
+ security_group_id = security_group["GroupId"]
+ print(f"--> Created Security Group in AWS with {security_group_id=}")
+ yield security_group_id
+ await ec2_client.delete_security_group(GroupId=security_group_id)
+ print(f"<-- Deleted Security Group in AWS with {security_group_id=}")
+
+
+@pytest.fixture
+async def aws_ami_id(
+ ec2_client: EC2Client,
+) -> str:
+ images = await ec2_client.describe_images()
+ image = random.choice(images["Images"]) # noqa: S311
+ assert "ImageId" in image
+ return image["ImageId"]
+
+
+@pytest.fixture
+def fake_ec2_instance_data(faker: Faker) -> Callable[..., EC2InstanceData]:
+ def _creator(**overrides) -> EC2InstanceData:
+ return EC2InstanceData(
+ **(
+ {
+ "launch_time": faker.date_time(tzinfo=datetime.timezone.utc),
+ "id": faker.uuid4(),
+ "aws_private_dns": f"ip-{faker.ipv4().replace('.', '-')}.ec2.internal",
+ "aws_public_ip": faker.ipv4(),
+ "type": faker.pystr(),
+ "state": faker.pystr(),
+ "resources": Resources(cpus=4.0, ram=ByteSize(1024 * 1024)),
+ "tags": faker.pydict(allowed_types=(str,)),
+ }
+ | overrides
+ )
+ )
+
+ return _creator
diff --git a/packages/pytest-simcore/src/pytest_simcore/aws_server.py b/packages/pytest-simcore/src/pytest_simcore/aws_server.py
new file mode 100644
index 00000000000..48baff0e4c2
--- /dev/null
+++ b/packages/pytest-simcore/src/pytest_simcore/aws_server.py
@@ -0,0 +1,86 @@
+# pylint: disable=redefined-outer-name
+# pylint: disable=unused-argument
+# pylint: disable=unused-import
+
+from collections.abc import Iterator
+
+import pytest
+import requests
+from aiohttp.test_utils import unused_port
+from moto.server import ThreadedMotoServer
+from settings_library.ec2 import EC2Settings
+
+from .helpers.utils_envs import EnvVarsDict, setenvs_from_dict
+from .helpers.utils_host import get_localhost_ip
+
+
+@pytest.fixture(scope="module")
+def mocked_aws_server() -> Iterator[ThreadedMotoServer]:
+ """creates a moto-server that emulates AWS services in place
+ NOTE: Never use a bucket with underscores it fails!!
+ """
+ server = ThreadedMotoServer(ip_address=get_localhost_ip(), port=unused_port())
+ # pylint: disable=protected-access
+ print(
+ f"--> started mock AWS server on {server._ip_address}:{server._port}" # noqa: SLF001
+ )
+ print(
+ f"--> Dashboard available on [http://{server._ip_address}:{server._port}/moto-api/]" # noqa: SLF001
+ )
+ server.start()
+ yield server
+ server.stop()
+ print(
+ f"<-- stopped mock AWS server on {server._ip_address}:{server._port}" # noqa: SLF001
+ )
+
+
+@pytest.fixture
+def reset_aws_server_state(mocked_aws_server: ThreadedMotoServer) -> Iterator[None]:
+ # NOTE: reset_aws_server_state [http://docs.getmoto.org/en/latest/docs/server_mode.html#reset-api]
+ yield
+ # pylint: disable=protected-access
+ requests.post(
+ f"http://{mocked_aws_server._ip_address}:{mocked_aws_server._port}/moto-api/reset", # noqa: SLF001
+ timeout=10,
+ )
+ print(
+ f"<-- cleaned mock AWS server on {mocked_aws_server._ip_address}:{mocked_aws_server._port}" # noqa: SLF001
+ )
+
+
+@pytest.fixture
+def mocked_ec2_server_settings(
+ mocked_aws_server: ThreadedMotoServer,
+ reset_aws_server_state: None,
+) -> EC2Settings:
+ return EC2Settings(
+ EC2_ACCESS_KEY_ID="xxx",
+ EC2_ENDPOINT=f"http://{mocked_aws_server._ip_address}:{mocked_aws_server._port}", # pylint: disable=protected-access # noqa: SLF001
+ EC2_SECRET_ACCESS_KEY="xxx", # noqa: S106
+ )
+
+
+@pytest.fixture
+def mocked_ec2_server_envs(
+ mocked_ec2_server_settings: EC2Settings,
+ monkeypatch: pytest.MonkeyPatch,
+) -> EnvVarsDict:
+ changed_envs: EnvVarsDict = mocked_ec2_server_settings.dict()
+ return setenvs_from_dict(monkeypatch, changed_envs)
+
+
+@pytest.fixture
+async def mocked_s3_server_envs(
+ mocked_aws_server: ThreadedMotoServer,
+ reset_aws_server_state: None,
+ monkeypatch: pytest.MonkeyPatch,
+) -> EnvVarsDict:
+ changed_envs = {
+ "S3_SECURE": "false",
+ "S3_ENDPOINT": f"{mocked_aws_server._ip_address}:{mocked_aws_server._port}", # pylint: disable=protected-access # noqa: SLF001
+ "S3_ACCESS_KEY": "xxx",
+ "S3_SECRET_KEY": "xxx",
+ "S3_BUCKET_NAME": "pytestbucket",
+ }
+ return setenvs_from_dict(monkeypatch, changed_envs)
diff --git a/packages/pytest-simcore/src/pytest_simcore/aws_services.py b/packages/pytest-simcore/src/pytest_simcore/aws_services.py
deleted file mode 100644
index b96bbfd6c98..00000000000
--- a/packages/pytest-simcore/src/pytest_simcore/aws_services.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# pylint: disable=protected-access
-# pylint: disable=redefined-outer-name
-# pylint: disable=unused-argument
-# pylint: disable=unused-variable
-
-import asyncio
-from typing import AsyncIterator, Iterator
-
-import pytest
-from aiobotocore.session import get_session
-from aiohttp.test_utils import unused_port
-from moto.server import ThreadedMotoServer
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
-
-
-@pytest.fixture(scope="module")
-def mocked_s3_server() -> Iterator[ThreadedMotoServer]:
- """creates a moto-server that emulates AWS services in place
- NOTE: Never use a bucket with underscores it fails!!
- """
- server = ThreadedMotoServer(ip_address=get_localhost_ip(), port=unused_port())
- # pylint: disable=protected-access
- print(f"--> started mock S3 server on {server._ip_address}:{server._port}")
- print(
- f"--> Dashboard available on [http://{server._ip_address}:{server._port}/moto-api/]"
- )
- server.start()
- yield server
- server.stop()
- print(f"<-- stopped mock S3 server on {server._ip_address}:{server._port}")
-
-
-async def _clean_bucket_content(aiobotore_s3_client, bucket: str):
- response = await aiobotore_s3_client.list_objects_v2(Bucket=bucket)
- while response["KeyCount"] > 0:
- await aiobotore_s3_client.delete_objects(
- Bucket=bucket,
- Delete={
- "Objects": [
- {"Key": obj["Key"]} for obj in response["Contents"] if "Key" in obj
- ]
- },
- )
- response = await aiobotore_s3_client.list_objects_v2(Bucket=bucket)
-
-
-async def _remove_all_buckets(aiobotore_s3_client):
- response = await aiobotore_s3_client.list_buckets()
- bucket_names = [
- bucket["Name"] for bucket in response["Buckets"] if "Name" in bucket
- ]
- await asyncio.gather(
- *(_clean_bucket_content(aiobotore_s3_client, bucket) for bucket in bucket_names)
- )
- await asyncio.gather(
- *(aiobotore_s3_client.delete_bucket(Bucket=bucket) for bucket in bucket_names)
- )
-
-
-@pytest.fixture
-async def mocked_s3_server_envs(
- mocked_s3_server: ThreadedMotoServer, monkeypatch: pytest.MonkeyPatch
-) -> AsyncIterator[None]:
- monkeypatch.setenv("S3_SECURE", "false")
- monkeypatch.setenv(
- "S3_ENDPOINT",
- f"{mocked_s3_server._ip_address}:{mocked_s3_server._port}", # pylint: disable=protected-access
- )
- monkeypatch.setenv("S3_ACCESS_KEY", "xxx")
- monkeypatch.setenv("S3_SECRET_KEY", "xxx")
- monkeypatch.setenv("S3_BUCKET_NAME", "pytestbucket")
-
- yield
-
- # cleanup the buckets
- session = get_session()
- async with session.create_client(
- "s3",
- endpoint_url=f"http://{mocked_s3_server._ip_address}:{mocked_s3_server._port}", # pylint: disable=protected-access
- aws_secret_access_key="xxx",
- aws_access_key_id="xxx",
- ) as client:
- await _remove_all_buckets(client)
diff --git a/packages/pytest-simcore/src/pytest_simcore/docker_compose.py b/packages/pytest-simcore/src/pytest_simcore/docker_compose.py
index db34d158938..424f6bb7da2 100644
--- a/packages/pytest-simcore/src/pytest_simcore/docker_compose.py
+++ b/packages/pytest-simcore/src/pytest_simcore/docker_compose.py
@@ -15,9 +15,10 @@
import shutil
import subprocess
import sys
+from collections.abc import Iterator
from copy import deepcopy
from pathlib import Path
-from typing import Any, Iterator
+from typing import Any
import pytest
import yaml
@@ -30,11 +31,8 @@
)
from .helpers.constants import HEADER_STR
from .helpers.typing_env import EnvVarsDict
-from .helpers.utils_docker import (
- get_localhost_ip,
- run_docker_compose_config,
- save_docker_infos,
-)
+from .helpers.utils_docker import run_docker_compose_config, save_docker_infos
+from .helpers.utils_host import get_localhost_ip
@pytest.fixture(scope="session")
@@ -79,9 +77,9 @@ def testing_environ_vars(env_devel_file: Path) -> EnvVarsDict:
env_devel["API_SERVER_DEV_FEATURES_ENABLED"] = "1"
- if not "DOCKER_REGISTRY" in os.environ:
+ if "DOCKER_REGISTRY" not in os.environ:
env_devel["DOCKER_REGISTRY"] = "local"
- if not "DOCKER_IMAGE_TAG" in os.environ:
+ if "DOCKER_IMAGE_TAG" not in os.environ:
env_devel["DOCKER_IMAGE_TAG"] = "production"
return {key: value for key, value in env_devel.items() if value is not None}
@@ -264,8 +262,7 @@ def core_docker_compose_file(
@pytest.fixture(scope="module")
def ops_services_selection(request) -> list[str]:
"""Selection of services from the ops stack"""
- ops_services = getattr(request.module, FIXTURE_CONFIG_OPS_SERVICES_SELECTION, [])
- return ops_services
+ return getattr(request.module, FIXTURE_CONFIG_OPS_SERVICES_SELECTION, [])
@pytest.fixture(scope="module")
@@ -370,7 +367,7 @@ def _filter_services_and_dump(
with docker_compose_path.open("wt") as fh:
if "TRAVIS" in os.environ:
# in travis we do not have access to file
- print(f"{str(docker_compose_path):-^100}")
+ print(f"{docker_compose_path!s:-^100}")
yaml.dump(content, sys.stdout, default_flow_style=False)
print("-" * 100)
else:
diff --git a/packages/pytest-simcore/src/pytest_simcore/docker_registry.py b/packages/pytest-simcore/src/pytest_simcore/docker_registry.py
index e063e23adb2..d1627698946 100644
--- a/packages/pytest-simcore/src/pytest_simcore/docker_registry.py
+++ b/packages/pytest-simcore/src/pytest_simcore/docker_registry.py
@@ -15,7 +15,7 @@
import tenacity
from pytest import FixtureRequest
-from .helpers.utils_docker import get_localhost_ip
+from .helpers.utils_host import get_localhost_ip
log = logging.getLogger(__name__)
diff --git a/packages/pytest-simcore/src/pytest_simcore/docker_swarm.py b/packages/pytest-simcore/src/pytest_simcore/docker_swarm.py
index 37b8c0379d8..e7a7fbe7c54 100644
--- a/packages/pytest-simcore/src/pytest_simcore/docker_swarm.py
+++ b/packages/pytest-simcore/src/pytest_simcore/docker_swarm.py
@@ -24,7 +24,7 @@
from .helpers.constants import HEADER_STR, MINUTE
from .helpers.typing_env import EnvVarsDict
from .helpers.utils_dict import copy_from_dict
-from .helpers.utils_docker import get_localhost_ip
+from .helpers.utils_host import get_localhost_ip
log = logging.getLogger(__name__)
diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/utils_docker.py b/packages/pytest-simcore/src/pytest_simcore/helpers/utils_docker.py
index 3d5502e6bfb..29b81200a7e 100644
--- a/packages/pytest-simcore/src/pytest_simcore/helpers/utils_docker.py
+++ b/packages/pytest-simcore/src/pytest_simcore/helpers/utils_docker.py
@@ -2,7 +2,6 @@
import logging
import os
import re
-import socket
import subprocess
from enum import Enum
from pathlib import Path
@@ -46,19 +45,6 @@ class ContainerStatus(str, Enum):
log = logging.getLogger(__name__)
-def get_localhost_ip(default="127.0.0.1") -> str:
- """Return the IP address for localhost"""
- local_ip = default
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- try:
- # doesn't even have to be reachable
- s.connect(("10.255.255.255", 1))
- local_ip = s.getsockname()[0]
- finally:
- s.close()
- return f"{local_ip}"
-
-
@retry(
wait=wait_fixed(2),
stop=stop_after_attempt(10),
diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/utils_host.py b/packages/pytest-simcore/src/pytest_simcore/helpers/utils_host.py
new file mode 100644
index 00000000000..e259053694e
--- /dev/null
+++ b/packages/pytest-simcore/src/pytest_simcore/helpers/utils_host.py
@@ -0,0 +1,14 @@
+import socket
+
+
+def get_localhost_ip(default="127.0.0.1") -> str:
+ """Return the IP address for localhost"""
+ local_ip = default
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ try:
+ # doesn't even have to be reachable
+ s.connect(("10.255.255.255", 1))
+ local_ip = s.getsockname()[0]
+ finally:
+ s.close()
+ return f"{local_ip}"
diff --git a/packages/pytest-simcore/src/pytest_simcore/httpbin_service.py b/packages/pytest-simcore/src/pytest_simcore/httpbin_service.py
index 901c2789519..62baba3623e 100644
--- a/packages/pytest-simcore/src/pytest_simcore/httpbin_service.py
+++ b/packages/pytest-simcore/src/pytest_simcore/httpbin_service.py
@@ -21,7 +21,7 @@
from tenacity.stop import stop_after_delay
from tenacity.wait import wait_fixed
-from .helpers.utils_docker import get_localhost_ip
+from .helpers.utils_host import get_localhost_ip
@pytest.fixture(scope="session")
diff --git a/packages/pytest-simcore/src/pytest_simcore/minio_service.py b/packages/pytest-simcore/src/pytest_simcore/minio_service.py
index c24c3d639dc..7ff284f9213 100644
--- a/packages/pytest-simcore/src/pytest_simcore/minio_service.py
+++ b/packages/pytest-simcore/src/pytest_simcore/minio_service.py
@@ -3,7 +3,8 @@
# pylint: disable=unused-variable
import logging
-from typing import Any, Iterator
+from collections.abc import Iterator
+from typing import Any
import pytest
from minio import Minio
@@ -16,7 +17,8 @@
from tenacity.stop import stop_after_attempt
from tenacity.wait import wait_fixed
-from .helpers.utils_docker import get_localhost_ip, get_service_published_port
+from .helpers.utils_docker import get_service_published_port
+from .helpers.utils_host import get_localhost_ip
log = logging.getLogger(__name__)
@@ -69,7 +71,6 @@ def minio_config(
@pytest.fixture(scope="module")
def minio_service(minio_config: dict[str, str]) -> Iterator[Minio]:
-
client = Minio(**minio_config["client"])
for attempt in Retrying(
diff --git a/packages/pytest-simcore/src/pytest_simcore/postgres_service.py b/packages/pytest-simcore/src/pytest_simcore/postgres_service.py
index ec250c00876..0c05f587652 100644
--- a/packages/pytest-simcore/src/pytest_simcore/postgres_service.py
+++ b/packages/pytest-simcore/src/pytest_simcore/postgres_service.py
@@ -15,7 +15,8 @@
from tenacity.stop import stop_after_delay
from tenacity.wait import wait_fixed
-from .helpers.utils_docker import get_localhost_ip, get_service_published_port
+from .helpers.utils_docker import get_service_published_port
+from .helpers.utils_host import get_localhost_ip
from .helpers.utils_postgres import PostgresTestConfig, migrated_pg_tables_context
_TEMPLATE_DB_TO_RESTORE = "template_simcore_db"
diff --git a/packages/pytest-simcore/src/pytest_simcore/rabbit_service.py b/packages/pytest-simcore/src/pytest_simcore/rabbit_service.py
index 3e90d2a7bcd..d190d711ff6 100644
--- a/packages/pytest-simcore/src/pytest_simcore/rabbit_service.py
+++ b/packages/pytest-simcore/src/pytest_simcore/rabbit_service.py
@@ -16,7 +16,8 @@
from tenacity.wait import wait_fixed
from .helpers.typing_env import EnvVarsDict
-from .helpers.utils_docker import get_localhost_ip, get_service_published_port
+from .helpers.utils_docker import get_service_published_port
+from .helpers.utils_host import get_localhost_ip
_logger = logging.getLogger(__name__)
diff --git a/packages/pytest-simcore/src/pytest_simcore/redis_service.py b/packages/pytest-simcore/src/pytest_simcore/redis_service.py
index 20cabcfe006..9651255198b 100644
--- a/packages/pytest-simcore/src/pytest_simcore/redis_service.py
+++ b/packages/pytest-simcore/src/pytest_simcore/redis_service.py
@@ -3,18 +3,20 @@
# pylint:disable=redefined-outer-name
import logging
-from typing import AsyncIterator
+from collections.abc import AsyncIterator
import pytest
import tenacity
from redis.asyncio import Redis, from_url
+from settings_library.basic_types import PortInt
from settings_library.redis import RedisDatabase, RedisSettings
from tenacity.before_sleep import before_sleep_log
from tenacity.stop import stop_after_delay
from tenacity.wait import wait_fixed
from yarl import URL
-from .helpers.utils_docker import get_localhost_ip, get_service_published_port
+from .helpers.utils_docker import get_service_published_port
+from .helpers.utils_host import get_localhost_ip
log = logging.getLogger(__name__)
@@ -33,13 +35,13 @@ async def redis_settings(
"simcore_redis", testing_environ_vars["REDIS_PORT"]
)
# test runner is running on the host computer
- settings = RedisSettings(REDIS_HOST=get_localhost_ip(), REDIS_PORT=int(port))
+ settings = RedisSettings(REDIS_HOST=get_localhost_ip(), REDIS_PORT=PortInt(port))
await wait_till_redis_responsive(settings.build_redis_dsn(RedisDatabase.RESOURCES))
return settings
-@pytest.fixture(scope="function")
+@pytest.fixture()
def redis_service(
redis_settings: RedisSettings,
monkeypatch: pytest.MonkeyPatch,
@@ -53,7 +55,7 @@ def redis_service(
return redis_settings
-@pytest.fixture(scope="function")
+@pytest.fixture()
async def redis_client(
redis_settings: RedisSettings,
) -> AsyncIterator[Redis]:
@@ -70,7 +72,7 @@ async def redis_client(
await client.close(close_connection_pool=True)
-@pytest.fixture(scope="function")
+@pytest.fixture()
async def redis_locks_client(
redis_settings: RedisSettings,
) -> AsyncIterator[Redis]:
@@ -98,6 +100,7 @@ async def wait_till_redis_responsive(redis_url: URL | str) -> None:
try:
if not await client.ping():
- raise ConnectionError(f"{redis_url=} not available")
+ msg = f"{redis_url=} not available"
+ raise ConnectionError(msg)
finally:
await client.close(close_connection_pool=True)
diff --git a/packages/pytest-simcore/src/pytest_simcore/simcore_services.py b/packages/pytest-simcore/src/pytest_simcore/simcore_services.py
index 34081e39670..406e1ac269e 100644
--- a/packages/pytest-simcore/src/pytest_simcore/simcore_services.py
+++ b/packages/pytest-simcore/src/pytest_simcore/simcore_services.py
@@ -20,7 +20,8 @@
from .helpers.constants import MINUTE
from .helpers.typing_env import EnvVarsDict
-from .helpers.utils_docker import get_localhost_ip, get_service_published_port
+from .helpers.utils_docker import get_service_published_port
+from .helpers.utils_host import get_localhost_ip
log = logging.getLogger(__name__)
diff --git a/packages/pytest-simcore/src/pytest_simcore/simcore_storage_service.py b/packages/pytest-simcore/src/pytest_simcore/simcore_storage_service.py
index 2e1c6f4dc86..9b0032c51fd 100644
--- a/packages/pytest-simcore/src/pytest_simcore/simcore_storage_service.py
+++ b/packages/pytest-simcore/src/pytest_simcore/simcore_storage_service.py
@@ -2,8 +2,8 @@
# pylint:disable=unused-argument
# pylint:disable=redefined-outer-name
import os
+from collections.abc import Callable, Iterable
from copy import deepcopy
-from typing import Callable, Iterable
import aiohttp
import pytest
@@ -15,7 +15,8 @@
from servicelib.minio_utils import MinioRetryPolicyUponInitialization
from yarl import URL
-from .helpers.utils_docker import get_localhost_ip, get_service_published_port
+from .helpers.utils_docker import get_service_published_port
+from .helpers.utils_host import get_localhost_ip
@pytest.fixture(scope="module")
@@ -38,7 +39,7 @@ def storage_endpoint(docker_stack: dict, testing_environ_vars: dict) -> Iterable
os.environ = old_environ
-@pytest.fixture(scope="function")
+@pytest.fixture()
async def storage_service(
minio_service: Minio, storage_endpoint: URL, docker_stack: dict
) -> URL:
diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/clusters_keeper/ec2_instances.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/clusters_keeper/ec2_instances.py
index 2663c82d520..433eee07fa3 100644
--- a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/clusters_keeper/ec2_instances.py
+++ b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/clusters_keeper/ec2_instances.py
@@ -1,5 +1,5 @@
from models_library.api_schemas_clusters_keeper import CLUSTERS_KEEPER_RPC_NAMESPACE
-from models_library.api_schemas_clusters_keeper.ec2_instances import EC2InstanceType
+from models_library.api_schemas_clusters_keeper.ec2_instances import EC2InstanceTypeGet
from models_library.rabbitmq_basic_types import RPCMethodName
from ..._client_rpc import RabbitMQRPCClient
@@ -8,14 +8,14 @@
async def get_instance_type_details(
client: RabbitMQRPCClient, *, instance_type_names: set[str]
-) -> list[EC2InstanceType]:
+) -> list[EC2InstanceTypeGet]:
"""**Remote method**
Raises:
RPCServerError -- if anything happens remotely
"""
- instance_types: list[EC2InstanceType] = await client.request(
+ instance_types: list[EC2InstanceTypeGet] = await client.request(
CLUSTERS_KEEPER_RPC_NAMESPACE,
RPCMethodName("get_instance_type_details"),
timeout_s=RPC_REMOTE_METHOD_TIMEOUT_S,
diff --git a/packages/settings-library/src/settings_library/ec2.py b/packages/settings-library/src/settings_library/ec2.py
new file mode 100644
index 00000000000..710db9c6e4f
--- /dev/null
+++ b/packages/settings-library/src/settings_library/ec2.py
@@ -0,0 +1,26 @@
+from typing import Any, ClassVar
+
+from pydantic import Field
+
+from .base import BaseCustomSettings
+
+
+class EC2Settings(BaseCustomSettings):
+ EC2_ACCESS_KEY_ID: str
+ EC2_ENDPOINT: str | None = Field(
+ default=None, description="do not define if using standard AWS"
+ )
+ EC2_REGION_NAME: str = "us-east-1"
+ EC2_SECRET_ACCESS_KEY: str
+
+ class Config(BaseCustomSettings.Config):
+ schema_extra: ClassVar[dict[str, Any]] = { # type: ignore[misc]
+ "examples": [
+ {
+ "EC2_ACCESS_KEY_ID": "my_access_key_id",
+ "EC2_ENDPOINT": "http://my_ec2_endpoint.com",
+ "EC2_REGION_NAME": "us-east-1",
+ "EC2_SECRET_ACCESS_KEY": "my_secret_access_key",
+ }
+ ],
+ }
diff --git a/packages/simcore-sdk/tests/conftest.py b/packages/simcore-sdk/tests/conftest.py
index f691a844b28..62a11df75ac 100644
--- a/packages/simcore-sdk/tests/conftest.py
+++ b/packages/simcore-sdk/tests/conftest.py
@@ -18,7 +18,7 @@
pytest_plugins = [
- "pytest_simcore.aws_services",
+ "pytest_simcore.aws_server",
"pytest_simcore.docker_compose",
"pytest_simcore.docker_swarm",
"pytest_simcore.file_extra",
diff --git a/packages/simcore-sdk/tests/unit/test_node_ports_common_file_io_utils.py b/packages/simcore-sdk/tests/unit/test_node_ports_common_file_io_utils.py
index 7c38863c096..12353b6cd6a 100644
--- a/packages/simcore-sdk/tests/unit/test_node_ports_common_file_io_utils.py
+++ b/packages/simcore-sdk/tests/unit/test_node_ports_common_file_io_utils.py
@@ -147,12 +147,12 @@ async def test_upload_file_to_presigned_links_raises_aws_s3_400_request_time_out
@pytest.fixture
async def aiobotocore_s3_client(
- mocked_s3_server: ThreadedMotoServer,
+ mocked_aws_server: ThreadedMotoServer,
) -> AsyncIterator[AioBaseClient]:
session = get_session()
async with session.create_client(
"s3",
- endpoint_url=f"http://{mocked_s3_server._ip_address}:{mocked_s3_server._port}", # pylint: disable=protected-access
+ endpoint_url=f"http://{mocked_aws_server._ip_address}:{mocked_aws_server._port}", # pylint: disable=protected-access
aws_secret_access_key="xxx",
aws_access_key_id="xxx",
) as client:
@@ -195,7 +195,7 @@ def file_id(faker: Faker) -> str:
@pytest.fixture
async def create_upload_links(
- mocked_s3_server: ThreadedMotoServer,
+ mocked_aws_server: ThreadedMotoServer,
aiobotocore_s3_client: AioBaseClient,
faker: Faker,
bucket: str,
diff --git a/services/agent/requirements/_base.in b/services/agent/requirements/_base.in
index c195fb17f5c..ea2c803f81b 100644
--- a/services/agent/requirements/_base.in
+++ b/services/agent/requirements/_base.in
@@ -14,4 +14,5 @@ aiodocker
fastapi
packaging
pydantic
+python-dotenv
uvicorn
diff --git a/services/agent/requirements/_base.txt b/services/agent/requirements/_base.txt
index 2619c197119..c3d0645fe49 100644
--- a/services/agent/requirements/_base.txt
+++ b/services/agent/requirements/_base.txt
@@ -115,6 +115,8 @@ pyrsistent==0.19.2
# via jsonschema
python-dateutil==2.8.2
# via arrow
+python-dotenv==1.0.0
+ # via -r requirements/_base.in
pyyaml==6.0.1
# via
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
diff --git a/services/agent/requirements/_test.txt b/services/agent/requirements/_test.txt
index af900eaf1d6..ac1398a936d 100644
--- a/services/agent/requirements/_test.txt
+++ b/services/agent/requirements/_test.txt
@@ -9,7 +9,9 @@ aioboto3==9.6.0
# -c requirements/./constraints.txt
# -r requirements/_test.in
aiobotocore==2.3.0
- # via aioboto3
+ # via
+ # aioboto3
+ # aiobotocore
aiohttp==3.8.5
# via
# -c requirements/../../../requirements/constraints.txt
@@ -229,7 +231,9 @@ python-dateutil==2.8.2
# faker
# moto
python-jose==3.3.0
- # via moto
+ # via
+ # moto
+ # python-jose
pyyaml==6.0.1
# via
# -c requirements/../../../requirements/constraints.txt
diff --git a/services/agent/tests/conftest.py b/services/agent/tests/conftest.py
index 92ec5d411c0..97da425621a 100644
--- a/services/agent/tests/conftest.py
+++ b/services/agent/tests/conftest.py
@@ -22,7 +22,7 @@
pytestmark = pytest.mark.asyncio
pytest_plugins = [
- "pytest_simcore.aws_services",
+ "pytest_simcore.aws_server",
"pytest_simcore.repository_paths",
]
@@ -188,9 +188,9 @@ def caplog_info_debug(caplog: LogCaptureFixture) -> Iterable[LogCaptureFixture]:
@pytest.fixture(scope="module")
-def mocked_s3_server_url(mocked_s3_server: ThreadedMotoServer) -> HttpUrl:
+def mocked_s3_server_url(mocked_aws_server: ThreadedMotoServer) -> HttpUrl:
# pylint: disable=protected-access
return parse_obj_as(
HttpUrl,
- f"http://{mocked_s3_server._ip_address}:{mocked_s3_server._port}", # noqa: SLF001
+ f"http://{mocked_aws_server._ip_address}:{mocked_aws_server._port}", # noqa: SLF001
)
diff --git a/services/api-server/tests/unit/conftest.py b/services/api-server/tests/unit/conftest.py
index b1046bafb27..3aa9a1d6cf1 100644
--- a/services/api-server/tests/unit/conftest.py
+++ b/services/api-server/tests/unit/conftest.py
@@ -36,8 +36,8 @@
from pydantic import HttpUrl, parse_obj_as
from pytest import MonkeyPatch # noqa: PT013
from pytest_mock.plugin import MockerFixture
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
from pytest_simcore.helpers.utils_envs import EnvVarsDict, setenvs_from_dict
+from pytest_simcore.helpers.utils_host import get_localhost_ip
from pytest_simcore.simcore_webserver_projects_rest_api import GET_PROJECT
from requests.auth import HTTPBasicAuth
from respx import MockRouter
@@ -269,7 +269,6 @@ def mocked_webserver_service_api_base(
assert_all_called=False,
assert_all_mocked=True,
) as respx_mock:
-
# healthcheck_readiness_probe, healthcheck_liveness_probe
response_body = {
"name": "webserver",
diff --git a/services/autoscaling/requirements/_base.in b/services/autoscaling/requirements/_base.in
index 09555b35987..ae362ec2744 100644
--- a/services/autoscaling/requirements/_base.in
+++ b/services/autoscaling/requirements/_base.in
@@ -9,14 +9,13 @@
# intra-repo required dependencies
--requirement ../../../packages/models-library/requirements/_base.in
--requirement ../../../packages/settings-library/requirements/_base.in
+--requirement ../../../packages/aws-library/requirements/_base.in
# service-library[fastapi]
--requirement ../../../packages/service-library/requirements/_base.in
--requirement ../../../packages/service-library/requirements/_fastapi.in
aiocache
aiodocker
-aioboto3
dask[distributed]
fastapi
packaging
-types-aiobotocore[ec2]
diff --git a/services/autoscaling/requirements/_base.txt b/services/autoscaling/requirements/_base.txt
index 0c81d2664c3..bb189297c0e 100644
--- a/services/autoscaling/requirements/_base.txt
+++ b/services/autoscaling/requirements/_base.txt
@@ -9,13 +9,15 @@ aio-pika==9.3.0
# -c requirements/../../../packages/service-library/requirements/./_base.in
# -r requirements/../../../packages/service-library/requirements/_base.in
aioboto3==12.0.0
- # via -r requirements/_base.in
+ # via -r requirements/../../../packages/aws-library/requirements/_base.in
aiobotocore==2.7.0
# via
# aioboto3
# aiobotocore
aiocache==0.12.2
- # via -r requirements/_base.in
+ # via
+ # -r requirements/../../../packages/aws-library/requirements/_base.in
+ # -r requirements/_base.in
aiodebug==2.3.0
# via
# -c requirements/../../../packages/service-library/requirements/./_base.in
@@ -31,6 +33,7 @@ aiofiles==23.2.1
# -r requirements/../../../packages/service-library/requirements/_base.in
aiohttp==3.8.6
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -79,6 +82,7 @@ botocore-stubs==1.31.77
# via types-aiobotocore
certifi==2023.7.22
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -121,6 +125,7 @@ exceptiongroup==1.1.3
# via anyio
fastapi==0.99.1
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -148,6 +153,7 @@ httpcore==0.18.0
# via httpx
httpx==0.25.0
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -170,6 +176,7 @@ importlib-metadata==6.8.0
# dask
jinja2==3.1.2
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -236,6 +243,7 @@ psutil==5.9.5
# distributed
pydantic==1.10.13
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -246,6 +254,7 @@ pydantic==1.10.13
# -c requirements/../../../packages/service-library/requirements/./_base.in
# -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../requirements/constraints.txt
+ # -r requirements/../../../packages/aws-library/requirements/_base.in
# -r requirements/../../../packages/models-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in
@@ -266,6 +275,7 @@ python-dateutil==2.8.2
# botocore
pyyaml==6.0.1
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -282,6 +292,7 @@ pyyaml==6.0.1
# distributed
redis==5.0.1
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -323,6 +334,7 @@ sortedcontainers==2.4.0
# distributed
starlette==0.27.0
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -363,7 +375,7 @@ typer==0.9.0
# -r requirements/../../../packages/service-library/requirements/./../../../packages/settings-library/requirements/_base.in
# -r requirements/../../../packages/settings-library/requirements/_base.in
types-aiobotocore==2.7.0
- # via -r requirements/_base.in
+ # via -r requirements/../../../packages/aws-library/requirements/_base.in
types-aiobotocore-ec2==2.7.0
# via types-aiobotocore
types-awscrt==0.19.8
@@ -382,6 +394,7 @@ typing-extensions==4.8.0
# uvicorn
urllib3==1.26.16
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
diff --git a/services/autoscaling/requirements/ci.txt b/services/autoscaling/requirements/ci.txt
index d2b1c3498e4..4adf7f199a7 100644
--- a/services/autoscaling/requirements/ci.txt
+++ b/services/autoscaling/requirements/ci.txt
@@ -11,11 +11,12 @@
--requirement _test.txt
# installs this repo's packages
+../../packages/aws-library
+../../packages/dask-task-models-library
../../packages/models-library
../../packages/pytest-simcore
../../packages/service-library[fastapi]
../../packages/settings-library
-../../packages/dask-task-models-library
# installs current package
.
diff --git a/services/autoscaling/requirements/dev.txt b/services/autoscaling/requirements/dev.txt
index 8cbfdbe8e3f..432e7ef62e9 100644
--- a/services/autoscaling/requirements/dev.txt
+++ b/services/autoscaling/requirements/dev.txt
@@ -12,6 +12,7 @@
--requirement _tools.txt
# installs this repo's packages
+--editable ../../packages/aws-library
--editable ../../packages/models-library
--editable ../../packages/pytest-simcore
--editable ../../packages/service-library[fastapi]
diff --git a/services/autoscaling/requirements/prod.txt b/services/autoscaling/requirements/prod.txt
index 7d635391e63..9452305726e 100644
--- a/services/autoscaling/requirements/prod.txt
+++ b/services/autoscaling/requirements/prod.txt
@@ -10,6 +10,7 @@
--requirement _base.txt
# installs this repo's packages
+../../packages/aws-library
../../packages/models-library
../../packages/service-library[fastapi]
../../packages/settings-library
diff --git a/services/autoscaling/src/simcore_service_autoscaling/core/settings.py b/services/autoscaling/src/simcore_service_autoscaling/core/settings.py
index 057d59aa912..be378286fad 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/core/settings.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/core/settings.py
@@ -21,6 +21,7 @@
)
from settings_library.base import BaseCustomSettings
from settings_library.docker_registry import RegistrySettings
+from settings_library.ec2 import EC2Settings
from settings_library.rabbit import RabbitSettings
from settings_library.redis import RedisSettings
from settings_library.utils_logging import MixinLoggingSettings
@@ -29,15 +30,6 @@
from .._meta import API_VERSION, API_VTAG, APP_NAME
-class EC2Settings(BaseCustomSettings):
- EC2_ACCESS_KEY_ID: str
- EC2_ENDPOINT: str | None = Field(
- default=None, description="do not define if using standard AWS"
- )
- EC2_REGION_NAME: str = "us-east-1"
- EC2_SECRET_ACCESS_KEY: str
-
-
class EC2InstancesSettings(BaseCustomSettings):
EC2_INSTANCES_ALLOWED_TYPES: list[str] = Field(
...,
diff --git a/services/autoscaling/src/simcore_service_autoscaling/models.py b/services/autoscaling/src/simcore_service_autoscaling/models.py
index 8be0f953fe8..9e3ffec2df9 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/models.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/models.py
@@ -1,65 +1,8 @@
-import datetime
from dataclasses import dataclass, field
from typing import Any, TypeAlias
+from aws_library.ec2.models import EC2InstanceData
from models_library.generated_models.docker_rest_api import Node
-from pydantic import BaseModel, ByteSize, NonNegativeFloat, PositiveInt
-from types_aiobotocore_ec2.literals import InstanceStateNameType, InstanceTypeType
-
-
-class Resources(BaseModel):
- cpus: NonNegativeFloat
- ram: ByteSize
-
- @classmethod
- def create_as_empty(cls) -> "Resources":
- return cls(cpus=0, ram=ByteSize(0))
-
- def __ge__(self, other: "Resources") -> bool:
- return self.cpus >= other.cpus and self.ram >= other.ram
-
- def __gt__(self, other: "Resources") -> bool:
- return self.cpus > other.cpus or self.ram > other.ram
-
- def __add__(self, other: "Resources") -> "Resources":
- return Resources.construct(
- **{
- key: a + b
- for (key, a), b in zip(
- self.dict().items(), other.dict().values(), strict=True
- )
- }
- )
-
- def __sub__(self, other: "Resources") -> "Resources":
- return Resources.construct(
- **{
- key: a - b
- for (key, a), b in zip(
- self.dict().items(), other.dict().values(), strict=True
- )
- }
- )
-
-
-@dataclass(frozen=True)
-class EC2InstanceType:
- name: InstanceTypeType
- cpus: PositiveInt
- ram: ByteSize
-
-
-InstancePrivateDNSName = str
-
-
-@dataclass(frozen=True)
-class EC2InstanceData:
- launch_time: datetime.datetime
- id: str
- aws_private_dns: InstancePrivateDNSName
- type: InstanceTypeType
- state: InstanceStateNameType
- resources: Resources
@dataclass(frozen=True)
diff --git a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_core.py b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_core.py
index 5dcd7de627c..382b7f85c6c 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_core.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_core.py
@@ -7,6 +7,12 @@
from typing import cast
import arrow
+from aws_library.ec2.models import (
+ EC2InstanceConfig,
+ EC2InstanceData,
+ EC2InstanceType,
+ Resources,
+)
from fastapi import FastAPI
from models_library.generated_models.docker_rest_api import (
Availability,
@@ -24,13 +30,7 @@
Ec2TooManyInstancesError,
)
from ..core.settings import ApplicationSettings, get_application_settings
-from ..models import (
- AssociatedInstance,
- Cluster,
- EC2InstanceData,
- EC2InstanceType,
- Resources,
-)
+from ..models import AssociatedInstance, Cluster
from ..utils import utils_docker, utils_ec2
from ..utils.auto_scaling_core import (
associate_ec2_instances_with_nodes,
@@ -58,12 +58,13 @@ async def _analyze_current_cluster(
# get the EC2 instances we have
existing_ec2_instances = await get_ec2_client(app).get_instances(
- app_settings.AUTOSCALING_EC2_INSTANCES, auto_scaling_mode.get_ec2_tags(app)
+ key_names=[app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_KEY_NAME],
+ tags=auto_scaling_mode.get_ec2_tags(app),
)
terminated_ec2_instances = await get_ec2_client(app).get_instances(
- app_settings.AUTOSCALING_EC2_INSTANCES,
- auto_scaling_mode.get_ec2_tags(app),
+ key_names=[app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_KEY_NAME],
+ tags=auto_scaling_mode.get_ec2_tags(app),
state_names=["terminated"],
)
@@ -112,7 +113,7 @@ def _node_not_ready(node: Node) -> bool:
async def _cleanup_disconnected_nodes(app: FastAPI, cluster: Cluster) -> Cluster:
if cluster.disconnected_nodes:
await utils_docker.remove_nodes(
- get_docker_client(app), cluster.disconnected_nodes
+ get_docker_client(app), nodes=cluster.disconnected_nodes
)
return dataclasses.replace(cluster, disconnected_nodes=[])
@@ -407,11 +408,17 @@ async def _start_instances(
results = await asyncio.gather(
*[
ec2_client.start_aws_instance(
- app_settings.AUTOSCALING_EC2_INSTANCES,
- instance_type=instance_type,
- tags=instance_tags,
- startup_script=instance_startup_script,
+ EC2InstanceConfig(
+ type=instance_type,
+ tags=instance_tags,
+ startup_script=instance_startup_script,
+ ami_id=app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_AMI_ID,
+ key_name=app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_KEY_NAME,
+ security_group_ids=app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_SECURITY_GROUP_IDS,
+ subnet_id=app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_SUBNET_ID,
+ ),
number_of_instances=instance_num,
+ max_number_of_instances=app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MAX_INSTANCES,
)
for instance_type, instance_num in needed_instances.items()
],
@@ -590,7 +597,7 @@ async def _try_scale_down_cluster(app: FastAPI, cluster: Cluster) -> Cluster:
await utils_docker.remove_nodes(
get_docker_client(app),
- [i.node for i in terminateable_instances],
+ nodes=[i.node for i in terminateable_instances],
force=True,
)
terminated_instance_ids = [i.ec2_instance.id for i in terminateable_instances]
diff --git a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_base.py b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_base.py
index 2adf6268e8c..08ae67d435f 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_base.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_base.py
@@ -2,13 +2,14 @@
from collections.abc import Iterable
from dataclasses import dataclass
+from aws_library.ec2.models import EC2InstanceData, EC2InstanceType, Resources
from fastapi import FastAPI
from models_library.docker import DockerLabelKey
from models_library.generated_models.docker_rest_api import Node as DockerNode
from servicelib.logging_utils import LogLevelInt
from types_aiobotocore_ec2.literals import InstanceTypeType
-from ..models import AssociatedInstance, EC2InstanceData, EC2InstanceType, Resources
+from ..models import AssociatedInstance
@dataclass
diff --git a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_computational.py b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_computational.py
index 0fe64471056..f627e0398a1 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_computational.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_computational.py
@@ -2,6 +2,7 @@
import logging
from collections.abc import Iterable
+from aws_library.ec2.models import EC2InstanceData, EC2InstanceType, Resources
from fastapi import FastAPI
from models_library.docker import (
DOCKER_TASK_EC2_INSTANCE_TYPE_PLACEMENT_CONSTRAINT_KEY,
@@ -14,13 +15,7 @@
from types_aiobotocore_ec2.literals import InstanceTypeType
from ..core.settings import get_application_settings
-from ..models import (
- AssociatedInstance,
- DaskTask,
- EC2InstanceData,
- EC2InstanceType,
- Resources,
-)
+from ..models import AssociatedInstance, DaskTask
from ..utils import computational_scaling as utils
from ..utils import utils_docker, utils_ec2
from . import dask
@@ -75,6 +70,7 @@ async def try_assigning_task_to_instances(
*,
notify_progress: bool
) -> bool:
+ assert type_to_instance_map # nosec
return await utils.try_assigning_task_to_instances(
app,
pending_task,
diff --git a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_dynamic.py b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_dynamic.py
index 6a2d5814c85..b2107f2a043 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_dynamic.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_dynamic.py
@@ -1,5 +1,6 @@
from collections.abc import Iterable
+from aws_library.ec2.models import EC2InstanceData, EC2InstanceType, Resources
from fastapi import FastAPI
from models_library.docker import DockerLabelKey
from models_library.generated_models.docker_rest_api import Node, Task
@@ -7,7 +8,7 @@
from types_aiobotocore_ec2.literals import InstanceTypeType
from ..core.settings import get_application_settings
-from ..models import AssociatedInstance, EC2InstanceData, EC2InstanceType, Resources
+from ..models import AssociatedInstance
from ..utils import dynamic_scaling as utils
from ..utils import utils_docker, utils_ec2
from ..utils.rabbitmq import log_tasks_message, progress_tasks_message
diff --git a/services/autoscaling/src/simcore_service_autoscaling/modules/dask.py b/services/autoscaling/src/simcore_service_autoscaling/modules/dask.py
index d94c855a60d..a40dacbb17e 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/modules/dask.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/modules/dask.py
@@ -4,6 +4,7 @@
from typing import Any, Final, TypeAlias
import distributed
+from aws_library.ec2.models import EC2InstanceData, Resources
from pydantic import AnyUrl, ByteSize, parse_obj_as
from ..core.errors import (
@@ -11,14 +12,7 @@
DaskSchedulerNotFoundError,
DaskWorkerNotFoundError,
)
-from ..models import (
- AssociatedInstance,
- DaskTask,
- DaskTaskId,
- DaskTaskResources,
- EC2InstanceData,
- Resources,
-)
+from ..models import AssociatedInstance, DaskTask, DaskTaskId, DaskTaskResources
from ..utils.auto_scaling_core import (
node_host_name_from_ec2_private_dns,
node_ip_from_ec2_private_dns,
diff --git a/services/autoscaling/src/simcore_service_autoscaling/modules/ec2.py b/services/autoscaling/src/simcore_service_autoscaling/modules/ec2.py
index f8c0eb595e1..c3464eb6f0b 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/modules/ec2.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/modules/ec2.py
@@ -1,232 +1,17 @@
-import contextlib
import logging
-from dataclasses import dataclass
from typing import cast
-import aioboto3
-import botocore.exceptions
-from aiobotocore.session import ClientCreatorContext
-from aiocache import cached
+from aws_library.ec2.client import SimcoreEC2API
from fastapi import FastAPI
-from pydantic import ByteSize, parse_obj_as
-from servicelib.logging_utils import log_context
+from settings_library.ec2 import EC2Settings
from tenacity._asyncio import AsyncRetrying
from tenacity.before_sleep import before_sleep_log
from tenacity.stop import stop_after_delay
from tenacity.wait import wait_random_exponential
-from types_aiobotocore_ec2 import EC2Client
-from types_aiobotocore_ec2.literals import InstanceStateNameType, InstanceTypeType
-from types_aiobotocore_ec2.type_defs import FilterTypeDef
-from ..core.errors import (
- ConfigurationError,
- Ec2InstanceNotFoundError,
- Ec2NotConnectedError,
- Ec2TooManyInstancesError,
-)
-from ..core.settings import EC2InstancesSettings, EC2Settings
-from ..models import EC2InstanceData, EC2InstanceType, Resources
-from ..utils.utils_ec2 import compose_user_data
+from ..core.errors import ConfigurationError, Ec2NotConnectedError
-logger = logging.getLogger(__name__)
-
-
-@dataclass(frozen=True)
-class AutoscalingEC2:
- client: EC2Client
- session: aioboto3.Session
- exit_stack: contextlib.AsyncExitStack
-
- @classmethod
- async def create(cls, settings: EC2Settings) -> "AutoscalingEC2":
- session = aioboto3.Session()
- session_client = session.client(
- "ec2",
- endpoint_url=settings.EC2_ENDPOINT,
- aws_access_key_id=settings.EC2_ACCESS_KEY_ID,
- aws_secret_access_key=settings.EC2_SECRET_ACCESS_KEY,
- region_name=settings.EC2_REGION_NAME,
- )
- assert isinstance(session_client, ClientCreatorContext) # nosec
- exit_stack = contextlib.AsyncExitStack()
- ec2_client = cast(
- EC2Client, await exit_stack.enter_async_context(session_client)
- )
- return cls(ec2_client, session, exit_stack)
-
- async def close(self) -> None:
- await self.exit_stack.aclose()
-
- async def ping(self) -> bool:
- try:
- await self.client.describe_account_attributes()
- return True
- except Exception: # pylint: disable=broad-except
- return False
-
- @cached(noself=True)
- async def get_ec2_instance_capabilities(
- self,
- instance_type_names: set[InstanceTypeType],
- ) -> list[EC2InstanceType]:
- """instance_type_names must be a set of unique values"""
- instance_types = await self.client.describe_instance_types(
- InstanceTypes=list(instance_type_names)
- )
- list_instances: list[EC2InstanceType] = []
- for instance in instance_types.get("InstanceTypes", []):
- with contextlib.suppress(KeyError):
- list_instances.append(
- EC2InstanceType(
- name=instance["InstanceType"],
- cpus=instance["VCpuInfo"]["DefaultVCpus"],
- ram=parse_obj_as(
- ByteSize, f"{instance['MemoryInfo']['SizeInMiB']}MiB"
- ),
- )
- )
- return list_instances
-
- async def start_aws_instance(
- self,
- instance_settings: EC2InstancesSettings,
- instance_type: EC2InstanceType,
- tags: dict[str, str],
- startup_script: str,
- number_of_instances: int,
- ) -> list[EC2InstanceData]:
- with log_context(
- logger,
- logging.INFO,
- msg=f"launching {number_of_instances} AWS instance(s) {instance_type.name} with {tags=}",
- ):
- # first check the max amount is not already reached
- current_instances = await self.get_instances(instance_settings, tags)
- if (
- len(current_instances) + number_of_instances
- > instance_settings.EC2_INSTANCES_MAX_INSTANCES
- ):
- raise Ec2TooManyInstancesError(
- num_instances=instance_settings.EC2_INSTANCES_MAX_INSTANCES
- )
-
- instances = await self.client.run_instances(
- ImageId=instance_settings.EC2_INSTANCES_AMI_ID,
- MinCount=number_of_instances,
- MaxCount=number_of_instances,
- InstanceType=instance_type.name,
- InstanceInitiatedShutdownBehavior="terminate",
- KeyName=instance_settings.EC2_INSTANCES_KEY_NAME,
- SubnetId=instance_settings.EC2_INSTANCES_SUBNET_ID,
- TagSpecifications=[
- {
- "ResourceType": "instance",
- "Tags": [
- {"Key": tag_key, "Value": tag_value}
- for tag_key, tag_value in tags.items()
- ],
- }
- ],
- UserData=compose_user_data(startup_script),
- SecurityGroupIds=instance_settings.EC2_INSTANCES_SECURITY_GROUP_IDS,
- )
- instance_ids = [i["InstanceId"] for i in instances["Instances"]]
- logger.info(
- "New instances launched: %s, waiting for them to start now...",
- instance_ids,
- )
-
- # wait for the instance to be in a pending state
- # NOTE: reference to EC2 states https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html
- waiter = self.client.get_waiter("instance_exists")
- await waiter.wait(InstanceIds=instance_ids)
- logger.info("instances %s exists now.", instance_ids)
-
- # get the private IPs
- instances = await self.client.describe_instances(InstanceIds=instance_ids)
- instance_datas = [
- EC2InstanceData(
- launch_time=instance["LaunchTime"],
- id=instance["InstanceId"],
- aws_private_dns=instance["PrivateDnsName"],
- type=instance["InstanceType"],
- state=instance["State"]["Name"],
- resources=Resources(cpus=instance_type.cpus, ram=instance_type.ram),
- )
- for instance in instances["Reservations"][0]["Instances"]
- ]
- logger.info(
- "%s is available, happy computing!!",
- f"{instance_datas=}",
- )
- return instance_datas
-
- async def get_instances(
- self,
- instance_settings: EC2InstancesSettings,
- tags: dict[str, str],
- *,
- state_names: list[InstanceStateNameType] | None = None,
- ) -> list[EC2InstanceData]:
- # NOTE: be careful: Name=instance-state-name,Values=["pending", "running"] means pending OR running
- # NOTE2: AND is done by repeating Name=instance-state-name,Values=pending Name=instance-state-name,Values=running
- if state_names is None:
- state_names = ["pending", "running"]
-
- filters: list[FilterTypeDef] = [
- {
- "Name": "key-name",
- "Values": [instance_settings.EC2_INSTANCES_KEY_NAME],
- },
- {"Name": "instance-state-name", "Values": state_names},
- ]
- filters.extend(
- [{"Name": f"tag:{key}", "Values": [value]} for key, value in tags.items()]
- )
-
- instances = await self.client.describe_instances(Filters=filters)
- all_instances = []
- for reservation in instances["Reservations"]:
- assert "Instances" in reservation # nosec
- for instance in reservation["Instances"]:
- assert "LaunchTime" in instance # nosec
- assert "InstanceId" in instance # nosec
- assert "PrivateDnsName" in instance # nosec
- assert "InstanceType" in instance # nosec
- assert "State" in instance # nosec
- assert "Name" in instance["State"] # nosec
- ec2_instance_types = await self.get_ec2_instance_capabilities(
- {instance["InstanceType"]}
- )
- assert len(ec2_instance_types) == 1 # nosec
- all_instances.append(
- EC2InstanceData(
- launch_time=instance["LaunchTime"],
- id=instance["InstanceId"],
- aws_private_dns=instance["PrivateDnsName"],
- type=instance["InstanceType"],
- state=instance["State"]["Name"],
- resources=Resources(
- cpus=ec2_instance_types[0].cpus,
- ram=ec2_instance_types[0].ram,
- ),
- )
- )
- logger.debug("received: %s", f"{all_instances=}")
- return all_instances
-
- async def terminate_instances(self, instance_datas: list[EC2InstanceData]) -> None:
- try:
- await self.client.terminate_instances(
- InstanceIds=[i.id for i in instance_datas]
- )
- except botocore.exceptions.ClientError as exc:
- if (
- exc.response.get("Error", {}).get("Code", "")
- == "InvalidInstanceID.NotFound"
- ):
- raise Ec2InstanceNotFoundError from exc
- raise # pragma: no cover
+_logger = logging.getLogger(__name__)
def setup(app: FastAPI) -> None:
@@ -235,16 +20,16 @@ async def on_startup() -> None:
settings: EC2Settings | None = app.state.settings.AUTOSCALING_EC2_ACCESS
if not settings:
- logger.warning("EC2 client is de-activated in the settings")
+ _logger.warning("EC2 client is de-activated in the settings")
return
- app.state.ec2_client = client = await AutoscalingEC2.create(settings)
+ app.state.ec2_client = client = await SimcoreEC2API.create(settings)
async for attempt in AsyncRetrying(
reraise=True,
stop=stop_after_delay(120),
wait=wait_random_exponential(max=30),
- before_sleep=before_sleep_log(logger, logging.WARNING),
+ before_sleep=before_sleep_log(_logger, logging.WARNING),
):
with attempt:
connected = await client.ping()
@@ -253,15 +38,15 @@ async def on_startup() -> None:
async def on_shutdown() -> None:
if app.state.ec2_client:
- await cast(AutoscalingEC2, app.state.ec2_client).close()
+ await cast(SimcoreEC2API, app.state.ec2_client).close()
app.add_event_handler("startup", on_startup)
app.add_event_handler("shutdown", on_shutdown)
-def get_ec2_client(app: FastAPI) -> AutoscalingEC2:
+def get_ec2_client(app: FastAPI) -> SimcoreEC2API:
if not app.state.ec2_client:
raise ConfigurationError(
msg="EC2 client is not available. Please check the configuration."
)
- return cast(AutoscalingEC2, app.state.ec2_client)
+ return cast(SimcoreEC2API, app.state.ec2_client)
diff --git a/services/autoscaling/src/simcore_service_autoscaling/utils/auto_scaling_core.py b/services/autoscaling/src/simcore_service_autoscaling/utils/auto_scaling_core.py
index 7d9ab4f310d..701f9d1ae39 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/utils/auto_scaling_core.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/utils/auto_scaling_core.py
@@ -3,12 +3,13 @@
import re
from typing import Final
+from aws_library.ec2.models import EC2InstanceData, EC2InstanceType, Resources
from models_library.generated_models.docker_rest_api import Node
from types_aiobotocore_ec2.literals import InstanceTypeType
from ..core.errors import Ec2InstanceInvalidError, Ec2InvalidDnsNameError
from ..core.settings import ApplicationSettings
-from ..models import AssociatedInstance, EC2InstanceData, EC2InstanceType, Resources
+from ..models import AssociatedInstance
from ..modules.auto_scaling_mode_base import BaseAutoscaling
from . import utils_docker
@@ -24,7 +25,8 @@ def node_host_name_from_ec2_private_dns(
Ec2InvalidDnsNameError: if the dns name does not follow the expected pattern
"""
if match := re.match(_EC2_INTERNAL_DNS_RE, ec2_instance_data.aws_private_dns):
- return match.group("host_name")
+ host_name: str = match.group("host_name")
+ return host_name
raise Ec2InvalidDnsNameError(aws_private_dns_name=ec2_instance_data.aws_private_dns)
diff --git a/services/autoscaling/src/simcore_service_autoscaling/utils/computational_scaling.py b/services/autoscaling/src/simcore_service_autoscaling/utils/computational_scaling.py
index f5cb43c4acb..8fcefb5c8de 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/utils/computational_scaling.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/utils/computational_scaling.py
@@ -3,19 +3,14 @@
from collections.abc import Iterable
from typing import Final
+from aws_library.ec2.models import EC2InstanceData, EC2InstanceType, Resources
from dask_task_models_library.constants import DASK_TASK_EC2_RESOURCE_RESTRICTION_KEY
from fastapi import FastAPI
from servicelib.utils_formatting import timedelta_as_minute_second
from types_aiobotocore_ec2.literals import InstanceTypeType
from ..core.settings import get_application_settings
-from ..models import (
- AssociatedInstance,
- DaskTask,
- EC2InstanceData,
- EC2InstanceType,
- Resources,
-)
+from ..models import AssociatedInstance, DaskTask
_logger = logging.getLogger(__name__)
diff --git a/services/autoscaling/src/simcore_service_autoscaling/utils/dynamic_scaling.py b/services/autoscaling/src/simcore_service_autoscaling/utils/dynamic_scaling.py
index 549b59bb38c..93af22f7f4e 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/utils/dynamic_scaling.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/utils/dynamic_scaling.py
@@ -2,12 +2,13 @@
import logging
from collections.abc import Iterable
+from aws_library.ec2.models import EC2InstanceData, EC2InstanceType, Resources
from fastapi import FastAPI
from models_library.generated_models.docker_rest_api import Task
from servicelib.utils_formatting import timedelta_as_minute_second
from ..core.settings import get_application_settings
-from ..models import AssociatedInstance, EC2InstanceData, EC2InstanceType, Resources
+from ..models import AssociatedInstance
from . import utils_docker
from .rabbitmq import log_tasks_message, progress_tasks_message
diff --git a/services/autoscaling/src/simcore_service_autoscaling/utils/rabbitmq.py b/services/autoscaling/src/simcore_service_autoscaling/utils/rabbitmq.py
index e8bce0ba5aa..d8c88c50a86 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/utils/rabbitmq.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/utils/rabbitmq.py
@@ -1,6 +1,7 @@
import asyncio
import logging
+from aws_library.ec2.models import Resources
from fastapi import FastAPI
from models_library.docker import StandardSimcoreDockerLabels
from models_library.generated_models.docker_rest_api import Task
@@ -13,7 +14,7 @@
from servicelib.logging_utils import log_catch
from ..core.settings import ApplicationSettings, get_application_settings
-from ..models import Cluster, Resources
+from ..models import Cluster
from ..modules.rabbitmq import post_message
logger = logging.getLogger(__name__)
diff --git a/services/autoscaling/src/simcore_service_autoscaling/utils/utils_docker.py b/services/autoscaling/src/simcore_service_autoscaling/utils/utils_docker.py
index 65b8ce8af05..edea9cee264 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/utils/utils_docker.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/utils/utils_docker.py
@@ -13,6 +13,7 @@
from typing import Final, cast
import yaml
+from aws_library.ec2.models import EC2InstanceData, Resources
from models_library.docker import (
DOCKER_TASK_EC2_INSTANCE_TYPE_PLACEMENT_CONSTRAINT_KEY,
DockerGenericTag,
@@ -33,7 +34,6 @@
from types_aiobotocore_ec2.literals import InstanceTypeType
from ..core.settings import ApplicationSettings
-from ..models import EC2InstanceData, Resources
from ..modules.docker import AutoscalingDocker
logger = logging.getLogger(__name__)
@@ -77,7 +77,7 @@ async def get_worker_nodes(docker_client: AutoscalingDocker) -> list[Node]:
async def remove_nodes(
- docker_client: AutoscalingDocker, nodes: list[Node], force: bool = False
+ docker_client: AutoscalingDocker, *, nodes: list[Node], force: bool = False
) -> list[Node]:
"""removes docker nodes that are in the down state (unless force is used and they will be forcibly removed)"""
diff --git a/services/autoscaling/src/simcore_service_autoscaling/utils/utils_ec2.py b/services/autoscaling/src/simcore_service_autoscaling/utils/utils_ec2.py
index 672a8c76733..88c18314460 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/utils/utils_ec2.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/utils/utils_ec2.py
@@ -8,10 +8,11 @@
from collections.abc import Callable
from textwrap import dedent
+from aws_library.ec2.models import EC2InstanceType, Resources
+
from .._meta import VERSION
from ..core.errors import ConfigurationError, Ec2InstanceNotFoundError
from ..core.settings import ApplicationSettings
-from ..models import EC2InstanceType, Resources
logger = logging.getLogger(__name__)
diff --git a/services/autoscaling/tests/unit/conftest.py b/services/autoscaling/tests/unit/conftest.py
index e388acba225..fb6dca9f814 100644
--- a/services/autoscaling/tests/unit/conftest.py
+++ b/services/autoscaling/tests/unit/conftest.py
@@ -6,10 +6,8 @@
import dataclasses
import datetime
import json
-import random
-from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
+from collections.abc import AsyncIterator, Awaitable, Callable
from copy import deepcopy
-from datetime import timezone
from pathlib import Path
from typing import Any, Final, cast
from unittest import mock
@@ -19,10 +17,10 @@
import httpx
import psutil
import pytest
-import requests
import simcore_service_autoscaling
-from aiohttp.test_utils import unused_port
from asgi_lifespan import LifespanManager
+from aws_library.ec2.client import SimcoreEC2API
+from aws_library.ec2.models import EC2InstanceData
from deepdiff import DeepDiff
from faker import Faker
from fakeredis.aioredis import FakeRedis
@@ -37,26 +35,25 @@
ResourceObject,
Service,
)
-from moto.server import ThreadedMotoServer
from pydantic import ByteSize, PositiveInt, parse_obj_as
from pytest_mock.plugin import MockerFixture
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
from pytest_simcore.helpers.utils_envs import EnvVarsDict, setenvs_from_dict
+from pytest_simcore.helpers.utils_host import get_localhost_ip
from settings_library.rabbit import RabbitSettings
from simcore_service_autoscaling.core.application import create_app
from simcore_service_autoscaling.core.settings import ApplicationSettings, EC2Settings
-from simcore_service_autoscaling.models import Cluster, DaskTaskResources, Resources
+from simcore_service_autoscaling.models import Cluster, DaskTaskResources
from simcore_service_autoscaling.modules.docker import AutoscalingDocker
-from simcore_service_autoscaling.modules.ec2 import AutoscalingEC2, EC2InstanceData
from tenacity import retry
from tenacity._asyncio import AsyncRetrying
from tenacity.retry import retry_if_exception_type
from tenacity.stop import stop_after_delay
from tenacity.wait import wait_fixed
-from types_aiobotocore_ec2.client import EC2Client
from types_aiobotocore_ec2.literals import InstanceTypeType
pytest_plugins = [
+ "pytest_simcore.aws_server",
+ "pytest_simcore.aws_ec2_service",
"pytest_simcore.dask_scheduler",
"pytest_simcore.docker_compose",
"pytest_simcore.docker_swarm",
@@ -114,6 +111,30 @@ def app_environment(
return mock_env_devel_environment | envs
+@pytest.fixture
+def mocked_ec2_instances_envs(
+ app_environment: EnvVarsDict,
+ monkeypatch: pytest.MonkeyPatch,
+ aws_security_group_id: str,
+ aws_subnet_id: str,
+ aws_ami_id: str,
+ aws_allowed_ec2_instance_type_names: list[InstanceTypeType],
+) -> EnvVarsDict:
+ envs = setenvs_from_dict(
+ monkeypatch,
+ {
+ "EC2_INSTANCES_KEY_NAME": "osparc-pytest",
+ "EC2_INSTANCES_SECURITY_GROUP_IDS": json.dumps([aws_security_group_id]),
+ "EC2_INSTANCES_SUBNET_ID": aws_subnet_id,
+ "EC2_INSTANCES_AMI_ID": aws_ami_id,
+ "EC2_INSTANCES_ALLOWED_TYPES": json.dumps(
+ aws_allowed_ec2_instance_type_names
+ ),
+ },
+ )
+ return app_environment | envs
+
+
@pytest.fixture
def disable_dynamic_service_background_task(mocker: MockerFixture) -> None:
mocker.patch(
@@ -460,53 +481,6 @@ async def assert_for_service_state(
)
-@pytest.fixture(scope="module")
-def mocked_aws_server() -> Iterator[ThreadedMotoServer]:
- """creates a moto-server that emulates AWS services in place
- NOTE: Never use a bucket with underscores it fails!!
- """
- server = ThreadedMotoServer(ip_address=get_localhost_ip(), port=unused_port())
- # pylint: disable=protected-access
- print(
- f"--> started mock AWS server on {server._ip_address}:{server._port}" # noqa: SLF001
- )
- print(
- f"--> Dashboard available on [http://{server._ip_address}:{server._port}/moto-api/]" # noqa: SLF001
- )
- server.start()
- yield server
- server.stop()
- print(
- f"<-- stopped mock AWS server on {server._ip_address}:{server._port}" # noqa: SLF001
- )
-
-
-@pytest.fixture
-def reset_aws_server_state(mocked_aws_server: ThreadedMotoServer) -> Iterator[None]:
- # NOTE: reset_aws_server_state [http://docs.getmoto.org/en/latest/docs/server_mode.html#reset-api]
- yield
- # pylint: disable=protected-access
- requests.post(
- f"http://{mocked_aws_server._ip_address}:{mocked_aws_server._port}/moto-api/reset", # noqa: SLF001
- timeout=10,
- )
-
-
-@pytest.fixture
-def mocked_aws_server_envs(
- app_environment: EnvVarsDict,
- mocked_aws_server: ThreadedMotoServer,
- reset_aws_server_state: None,
- monkeypatch: pytest.MonkeyPatch,
-) -> EnvVarsDict:
- changed_envs: EnvVarsDict = {
- "EC2_ENDPOINT": f"http://{mocked_aws_server._ip_address}:{mocked_aws_server._port}", # pylint: disable=protected-access # noqa: SLF001
- "EC2_ACCESS_KEY_ID": "xxx",
- "EC2_SECRET_ACCESS_KEY": "xxx",
- }
- return app_environment | setenvs_from_dict(monkeypatch, changed_envs)
-
-
@pytest.fixture(scope="session")
def aws_allowed_ec2_instance_type_names() -> list[InstanceTypeType]:
return [
@@ -532,125 +506,17 @@ def aws_allowed_ec2_instance_type_names_env(
return app_environment | setenvs_from_dict(monkeypatch, changed_envs)
-@pytest.fixture(scope="session")
-def vpc_cidr_block() -> str:
- return "10.0.0.0/16"
-
-
-@pytest.fixture
-async def aws_vpc_id(
- mocked_aws_server_envs: None,
- app_environment: EnvVarsDict,
- monkeypatch: pytest.MonkeyPatch,
- ec2_client: EC2Client,
- vpc_cidr_block: str,
-) -> AsyncIterator[str]:
- vpc = await ec2_client.create_vpc(
- CidrBlock=vpc_cidr_block,
- )
- vpc_id = vpc["Vpc"]["VpcId"] # type: ignore
- print(f"--> Created Vpc in AWS with {vpc_id=}")
- yield vpc_id
-
- await ec2_client.delete_vpc(VpcId=vpc_id)
- print(f"<-- Deleted Vpc in AWS with {vpc_id=}")
-
-
-@pytest.fixture(scope="session")
-def subnet_cidr_block() -> str:
- return "10.0.1.0/24"
-
-
-@pytest.fixture
-async def aws_subnet_id(
- monkeypatch: pytest.MonkeyPatch,
- aws_vpc_id: str,
- ec2_client: EC2Client,
- subnet_cidr_block: str,
-) -> AsyncIterator[str]:
- subnet = await ec2_client.create_subnet(
- CidrBlock=subnet_cidr_block, VpcId=aws_vpc_id
- )
- assert "Subnet" in subnet
- assert "SubnetId" in subnet["Subnet"]
- subnet_id = subnet["Subnet"]["SubnetId"]
- print(f"--> Created Subnet in AWS with {subnet_id=}")
-
- monkeypatch.setenv("EC2_INSTANCES_SUBNET_ID", subnet_id)
- yield subnet_id
-
- # all the instances in the subnet must be terminated before that works
- instances_in_subnet = await ec2_client.describe_instances(
- Filters=[{"Name": "subnet-id", "Values": [subnet_id]}]
- )
- if instances_in_subnet["Reservations"]:
- print(f"--> terminating {len(instances_in_subnet)} instances in subnet")
- await ec2_client.terminate_instances(
- InstanceIds=[
- instance["Instances"][0]["InstanceId"] # type: ignore
- for instance in instances_in_subnet["Reservations"]
- ]
- )
- print(f"<-- terminated {len(instances_in_subnet)} instances in subnet")
-
- await ec2_client.delete_subnet(SubnetId=subnet_id)
- subnets = await ec2_client.describe_subnets()
- print(f"<-- Deleted Subnet in AWS with {subnet_id=}")
- print(f"current {subnets=}")
-
-
-@pytest.fixture
-async def aws_security_group_id(
- monkeypatch: pytest.MonkeyPatch,
- faker: Faker,
- aws_vpc_id: str,
- ec2_client: EC2Client,
-) -> AsyncIterator[str]:
- security_group = await ec2_client.create_security_group(
- Description=faker.text(), GroupName=faker.pystr(), VpcId=aws_vpc_id
- )
- security_group_id = security_group["GroupId"]
- print(f"--> Created Security Group in AWS with {security_group_id=}")
- monkeypatch.setenv(
- "EC2_INSTANCES_SECURITY_GROUP_IDS", json.dumps([security_group_id])
- )
- yield security_group_id
- await ec2_client.delete_security_group(GroupId=security_group_id)
- print(f"<-- Deleted Security Group in AWS with {security_group_id=}")
-
-
-@pytest.fixture
-async def aws_ami_id(
- app_environment: EnvVarsDict,
- mocked_aws_server_envs: None,
- monkeypatch: pytest.MonkeyPatch,
- ec2_client: EC2Client,
-) -> str:
- images = await ec2_client.describe_images()
- image = random.choice(images["Images"]) # noqa: S311
- ami_id = image["ImageId"] # type: ignore
- monkeypatch.setenv("EC2_INSTANCES_AMI_ID", ami_id)
- return ami_id
-
-
@pytest.fixture
async def autoscaling_ec2(
app_environment: EnvVarsDict,
-) -> AsyncIterator[AutoscalingEC2]:
+) -> AsyncIterator[SimcoreEC2API]:
settings = EC2Settings.create_from_envs()
- ec2 = await AutoscalingEC2.create(settings)
+ ec2 = await SimcoreEC2API.create(settings)
assert ec2
yield ec2
await ec2.close()
-@pytest.fixture
-async def ec2_client(
- autoscaling_ec2: AutoscalingEC2,
-) -> EC2Client:
- return autoscaling_ec2.client
-
-
@pytest.fixture
def host_cpu_count() -> int:
return psutil.cpu_count()
@@ -679,26 +545,6 @@ def aws_instance_private_dns() -> str:
return "ip-10-23-40-12.ec2.internal"
-@pytest.fixture
-def fake_ec2_instance_data(faker: Faker) -> Callable[..., EC2InstanceData]:
- def _creator(**overrides) -> EC2InstanceData:
- return EC2InstanceData(
- **(
- {
- "launch_time": faker.date_time(tzinfo=timezone.utc),
- "id": faker.uuid4(),
- "aws_private_dns": f"ip-{faker.ipv4().replace('.', '-')}.ec2.internal",
- "type": faker.pystr(),
- "state": faker.pystr(),
- "resources": Resources(cpus=4.0, ram=ByteSize(1024 * 1024)),
- }
- | overrides
- )
- )
-
- return _creator
-
-
@pytest.fixture
def fake_localhost_ec2_instance_data(
fake_ec2_instance_data: Callable[..., EC2InstanceData]
diff --git a/services/autoscaling/tests/unit/test_api_health.py b/services/autoscaling/tests/unit/test_api_health.py
index 1ad9bbda13c..9ac0dccac01 100644
--- a/services/autoscaling/tests/unit/test_api_health.py
+++ b/services/autoscaling/tests/unit/test_api_health.py
@@ -20,7 +20,7 @@
def app_environment(
app_environment: EnvVarsDict,
enabled_rabbitmq: None,
- mocked_aws_server_envs: None,
+ mocked_ec2_server_envs: EnvVarsDict,
mocked_redis_server: None,
) -> EnvVarsDict:
return app_environment
diff --git a/services/autoscaling/tests/unit/test_core_settings.py b/services/autoscaling/tests/unit/test_core_settings.py
index 45376d5963e..8576d5fdb35 100644
--- a/services/autoscaling/tests/unit/test_core_settings.py
+++ b/services/autoscaling/tests/unit/test_core_settings.py
@@ -56,16 +56,16 @@ def test_invalid_EC2_INSTANCES_TIME_BEFORE_TERMINATION(
assert settings.AUTOSCALING_EC2_INSTANCES
assert settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_TIME_BEFORE_TERMINATION
assert (
- settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_TIME_BEFORE_TERMINATION
- == datetime.timedelta(minutes=59)
+ datetime.timedelta(minutes=59)
+ == settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_TIME_BEFORE_TERMINATION
)
monkeypatch.setenv("EC2_INSTANCES_TIME_BEFORE_TERMINATION", "-1:05:00")
settings = ApplicationSettings.create_from_envs()
assert settings.AUTOSCALING_EC2_INSTANCES
assert (
- settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_TIME_BEFORE_TERMINATION
- == datetime.timedelta(minutes=0)
+ datetime.timedelta(minutes=0)
+ == settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_TIME_BEFORE_TERMINATION
)
@@ -74,7 +74,7 @@ def test_EC2_INSTANCES_PRE_PULL_IMAGES(
):
settings = ApplicationSettings.create_from_envs()
assert settings.AUTOSCALING_EC2_INSTANCES
- assert settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_PRE_PULL_IMAGES == []
+ assert not settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_PRE_PULL_IMAGES
# passing an invalid image tag name will fail
monkeypatch.setenv(
@@ -97,9 +97,9 @@ def test_EC2_INSTANCES_PRE_PULL_IMAGES(
)
settings = ApplicationSettings.create_from_envs()
assert settings.AUTOSCALING_EC2_INSTANCES
- assert settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_PRE_PULL_IMAGES == [
+ assert [
"nginx:latest",
"itisfoundation/my-very-nice-service:latest",
"simcore/services/dynamic/another-nice-one:2.4.5",
"asd",
- ]
+ ] == settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_PRE_PULL_IMAGES
diff --git a/services/autoscaling/tests/unit/test_models.py b/services/autoscaling/tests/unit/test_models.py
index fdb9362591a..f859ff591d6 100644
--- a/services/autoscaling/tests/unit/test_models.py
+++ b/services/autoscaling/tests/unit/test_models.py
@@ -3,92 +3,14 @@
# pylint: disable=unused-variable
-from typing import Any, Awaitable, Callable
+from collections.abc import Awaitable, Callable
+from typing import Any
import aiodocker
import pytest
from models_library.docker import DockerLabelKey, StandardSimcoreDockerLabels
from models_library.generated_models.docker_rest_api import Service, Task
-from pydantic import ByteSize, ValidationError, parse_obj_as
-from simcore_service_autoscaling.models import Resources
-
-
-@pytest.mark.parametrize(
- "a,b,a_greater_or_equal_than_b",
- [
- (
- Resources(cpus=0.2, ram=ByteSize(0)),
- Resources(cpus=0.1, ram=ByteSize(0)),
- True,
- ),
- (
- Resources(cpus=0.1, ram=ByteSize(0)),
- Resources(cpus=0.1, ram=ByteSize(0)),
- True,
- ),
- (
- Resources(cpus=0.1, ram=ByteSize(1)),
- Resources(cpus=0.1, ram=ByteSize(0)),
- True,
- ),
- (
- Resources(cpus=0.05, ram=ByteSize(1)),
- Resources(cpus=0.1, ram=ByteSize(0)),
- False,
- ),
- (
- Resources(cpus=0.1, ram=ByteSize(0)),
- Resources(cpus=0.1, ram=ByteSize(1)),
- False,
- ),
- ],
-)
-def test_resources_ge_operator(
- a: Resources, b: Resources, a_greater_or_equal_than_b: bool
-):
- assert (a >= b) is a_greater_or_equal_than_b
-
-
-@pytest.mark.parametrize(
- "a,b,result",
- [
- (
- Resources(cpus=0, ram=ByteSize(0)),
- Resources(cpus=1, ram=ByteSize(34)),
- Resources(cpus=1, ram=ByteSize(34)),
- ),
- (
- Resources(cpus=0.1, ram=ByteSize(-1)),
- Resources(cpus=1, ram=ByteSize(34)),
- Resources(cpus=1.1, ram=ByteSize(33)),
- ),
- ],
-)
-def test_resources_add(a: Resources, b: Resources, result: Resources):
- assert a + b == result
- a += b
- assert a == result
-
-
-@pytest.mark.parametrize(
- "a,b,result",
- [
- (
- Resources(cpus=0, ram=ByteSize(0)),
- Resources(cpus=1, ram=ByteSize(34)),
- Resources.construct(cpus=-1, ram=ByteSize(-34)),
- ),
- (
- Resources(cpus=0.1, ram=ByteSize(-1)),
- Resources(cpus=1, ram=ByteSize(34)),
- Resources.construct(cpus=-0.9, ram=ByteSize(-35)),
- ),
- ],
-)
-def test_resources_sub(a: Resources, b: Resources, result: Resources):
- assert a - b == result
- a -= b
- assert a == result
+from pydantic import ValidationError, parse_obj_as
async def test_get_simcore_service_docker_labels_from_task_with_missing_labels_raises(
diff --git a/services/autoscaling/tests/unit/test_modules_auto_scaling_computational.py b/services/autoscaling/tests/unit/test_modules_auto_scaling_computational.py
index 0482437bb5d..7d4f1755ff3 100644
--- a/services/autoscaling/tests/unit/test_modules_auto_scaling_computational.py
+++ b/services/autoscaling/tests/unit/test_modules_auto_scaling_computational.py
@@ -18,6 +18,7 @@
import distributed
import pytest
+from aws_library.ec2.models import Resources
from dask_task_models_library.constants import DASK_TASK_EC2_RESOURCE_RESTRICTION_KEY
from faker import Faker
from fastapi import FastAPI
@@ -34,7 +35,6 @@
AssociatedInstance,
Cluster,
EC2InstanceData,
- Resources,
)
from simcore_service_autoscaling.modules.auto_scaling_core import (
_deactivate_empty_nodes,
@@ -66,14 +66,12 @@ def local_dask_scheduler_server_envs(
@pytest.fixture
def minimal_configuration(
docker_swarm: None,
+ mocked_ec2_server_envs: EnvVarsDict,
enabled_computational_mode: EnvVarsDict,
local_dask_scheduler_server_envs: EnvVarsDict,
+ mocked_ec2_instances_envs: EnvVarsDict,
disabled_rabbitmq: None,
disable_dynamic_service_background_task: None,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- aws_allowed_ec2_instance_type_names_env: list[str],
mocked_redis_server: None,
) -> None:
...
@@ -90,11 +88,6 @@ def dask_workers_config() -> dict[str, Any]:
}
-@pytest.fixture
-def empty_cluster(cluster: Callable[..., Cluster]) -> Cluster:
- return cluster()
-
-
async def _assert_ec2_instances(
ec2_client: EC2Client,
*,
@@ -192,15 +185,6 @@ def mock_find_node_with_name(
)
-@pytest.fixture
-def mock_cluster_used_resources(mocker: MockerFixture) -> mock.Mock:
- return mocker.patch(
- "simcore_service_autoscaling.modules.auto_scaling_core.utils_docker.compute_cluster_used_resources",
- autospec=True,
- return_value=Resources.create_as_empty(),
- )
-
-
@pytest.fixture
def mock_compute_node_used_resources(mocker: MockerFixture) -> mock.Mock:
return mocker.patch(
@@ -220,7 +204,7 @@ def mock_rabbitmq_post_message(mocker: MockerFixture) -> Iterator[mock.Mock]:
@pytest.fixture
def mock_terminate_instances(mocker: MockerFixture) -> Iterator[mock.Mock]:
return mocker.patch(
- "simcore_service_autoscaling.modules.ec2.AutoscalingEC2.terminate_instances",
+ "simcore_service_autoscaling.modules.ec2.SimcoreEC2API.terminate_instances",
autospec=True,
)
@@ -232,7 +216,7 @@ def mock_start_aws_instance(
fake_ec2_instance_data: Callable[..., EC2InstanceData],
) -> Iterator[mock.Mock]:
return mocker.patch(
- "simcore_service_autoscaling.modules.ec2.AutoscalingEC2.start_aws_instance",
+ "simcore_service_autoscaling.modules.ec2.SimcoreEC2API.start_aws_instance",
autospec=True,
return_value=fake_ec2_instance_data(aws_private_dns=aws_instance_private_dns),
)
@@ -556,7 +540,9 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
- datetime.timedelta(seconds=1)
).isoformat()
await auto_scale_cluster(app=initialized_app, auto_scaling_mode=auto_scaling_mode)
- mocked_docker_remove_node.assert_called_once_with(mock.ANY, [fake_node], force=True)
+ mocked_docker_remove_node.assert_called_once_with(
+ mock.ANY, nodes=[fake_node], force=True
+ )
await _assert_ec2_instances(
ec2_client,
num_reservations=1,
diff --git a/services/autoscaling/tests/unit/test_modules_auto_scaling_dynamic.py b/services/autoscaling/tests/unit/test_modules_auto_scaling_dynamic.py
index cb41d2d3278..3f4caf35d1d 100644
--- a/services/autoscaling/tests/unit/test_modules_auto_scaling_dynamic.py
+++ b/services/autoscaling/tests/unit/test_modules_auto_scaling_dynamic.py
@@ -16,6 +16,7 @@
import aiodocker
import pytest
+from aws_library.ec2.models import EC2InstanceData, Resources
from faker import Faker
from fastapi import FastAPI
from models_library.docker import (
@@ -35,7 +36,7 @@
from pytest_mock.plugin import MockerFixture
from pytest_simcore.helpers.utils_envs import EnvVarsDict
from simcore_service_autoscaling.core.settings import ApplicationSettings
-from simcore_service_autoscaling.models import AssociatedInstance, Cluster, Resources
+from simcore_service_autoscaling.models import AssociatedInstance, Cluster
from simcore_service_autoscaling.modules.auto_scaling_core import (
_activate_drained_nodes,
_deactivate_empty_nodes,
@@ -50,7 +51,6 @@
AutoscalingDocker,
get_docker_client,
)
-from simcore_service_autoscaling.modules.ec2 import EC2InstanceData
from types_aiobotocore_ec2.client import EC2Client
from types_aiobotocore_ec2.literals import InstanceTypeType
@@ -58,7 +58,7 @@
@pytest.fixture
def mock_terminate_instances(mocker: MockerFixture) -> Iterator[mock.Mock]:
return mocker.patch(
- "simcore_service_autoscaling.modules.ec2.AutoscalingEC2.terminate_instances",
+ "simcore_service_autoscaling.modules.ec2.SimcoreEC2API.terminate_instances",
autospec=True,
)
@@ -70,7 +70,7 @@ def mock_start_aws_instance(
fake_ec2_instance_data: Callable[..., EC2InstanceData],
) -> Iterator[mock.Mock]:
return mocker.patch(
- "simcore_service_autoscaling.modules.ec2.AutoscalingEC2.start_aws_instance",
+ "simcore_service_autoscaling.modules.ec2.SimcoreEC2API.start_aws_instance",
autospec=True,
return_value=fake_ec2_instance_data(aws_private_dns=aws_instance_private_dns),
)
@@ -114,15 +114,6 @@ def mock_remove_nodes(mocker: MockerFixture) -> mock.Mock:
)
-@pytest.fixture
-def mock_cluster_used_resources(mocker: MockerFixture) -> mock.Mock:
- return mocker.patch(
- "simcore_service_autoscaling.modules.auto_scaling_core.utils_docker.compute_cluster_used_resources",
- autospec=True,
- return_value=Resources.create_as_empty(),
- )
-
-
@pytest.fixture
def mock_compute_node_used_resources(mocker: MockerFixture) -> mock.Mock:
return mocker.patch(
@@ -197,13 +188,11 @@ async def drained_host_node(
@pytest.fixture
def minimal_configuration(
docker_swarm: None,
+ mocked_ec2_server_envs: EnvVarsDict,
enabled_dynamic_mode: EnvVarsDict,
+ mocked_ec2_instances_envs: EnvVarsDict,
disabled_rabbitmq: None,
disable_dynamic_service_background_task: None,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- aws_allowed_ec2_instance_type_names_env: list[str],
mocked_redis_server: None,
) -> None:
...
@@ -676,7 +665,9 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
- datetime.timedelta(seconds=1)
).isoformat()
await auto_scale_cluster(app=initialized_app, auto_scaling_mode=auto_scaling_mode)
- mocked_docker_remove_node.assert_called_once_with(mock.ANY, [fake_node], force=True)
+ mocked_docker_remove_node.assert_called_once_with(
+ mock.ANY, nodes=[fake_node], force=True
+ )
await _assert_ec2_instances(
ec2_client,
num_reservations=1,
diff --git a/services/autoscaling/tests/unit/test_modules_auto_scaling_task.py b/services/autoscaling/tests/unit/test_modules_auto_scaling_task.py
index ceb7816531b..7f3663ef7dc 100644
--- a/services/autoscaling/tests/unit/test_modules_auto_scaling_task.py
+++ b/services/autoscaling/tests/unit/test_modules_auto_scaling_task.py
@@ -20,7 +20,7 @@
def app_environment(
app_environment: EnvVarsDict,
disabled_rabbitmq: None,
- mocked_aws_server_envs: None,
+ mocked_ec2_server_envs: None,
mocked_redis_server: None,
monkeypatch: pytest.MonkeyPatch,
) -> EnvVarsDict:
diff --git a/services/autoscaling/tests/unit/test_modules_dask.py b/services/autoscaling/tests/unit/test_modules_dask.py
index 561ed1755d3..09b23f84b33 100644
--- a/services/autoscaling/tests/unit/test_modules_dask.py
+++ b/services/autoscaling/tests/unit/test_modules_dask.py
@@ -9,6 +9,7 @@
import distributed
import pytest
+from aws_library.ec2.models import Resources
from faker import Faker
from pydantic import AnyUrl, ByteSize, parse_obj_as
from simcore_service_autoscaling.core.errors import (
@@ -21,7 +22,6 @@
DaskTaskId,
DaskTaskResources,
EC2InstanceData,
- Resources,
)
from simcore_service_autoscaling.modules.dask import (
DaskTask,
diff --git a/services/autoscaling/tests/unit/test_modules_ec2.py b/services/autoscaling/tests/unit/test_modules_ec2.py
index b64b8517f5c..0be93d22055 100644
--- a/services/autoscaling/tests/unit/test_modules_ec2.py
+++ b/services/autoscaling/tests/unit/test_modules_ec2.py
@@ -2,68 +2,10 @@
# pylint: disable=unused-argument
# pylint: disable=unused-variable
-from collections.abc import Callable
-from typing import cast
-
-import botocore.exceptions
import pytest
-from faker import Faker
from fastapi import FastAPI
-from moto.server import ThreadedMotoServer
-from pydantic import ByteSize, parse_obj_as
-from pytest_simcore.helpers.utils_envs import EnvVarsDict
-from simcore_service_autoscaling.core.errors import (
- ConfigurationError,
- Ec2InstanceNotFoundError,
- Ec2TooManyInstancesError,
-)
-from simcore_service_autoscaling.core.settings import ApplicationSettings, EC2Settings
-from simcore_service_autoscaling.models import EC2InstanceType
-from simcore_service_autoscaling.modules.ec2 import (
- AutoscalingEC2,
- EC2InstanceData,
- get_ec2_client,
-)
-from types_aiobotocore_ec2 import EC2Client
-from types_aiobotocore_ec2.literals import InstanceTypeType
-
-
-@pytest.fixture
-def ec2_settings(
- app_environment: EnvVarsDict,
-) -> EC2Settings:
- return EC2Settings.create_from_envs()
-
-
-@pytest.fixture
-def app_settings(
- app_environment: EnvVarsDict,
-) -> ApplicationSettings:
- return ApplicationSettings.create_from_envs()
-
-
-async def test_ec2_client_lifespan(ec2_settings: EC2Settings):
- ec2 = await AutoscalingEC2.create(settings=ec2_settings)
- assert ec2
- assert ec2.client
- assert ec2.exit_stack
- assert ec2.session
-
- await ec2.close()
-
-
-async def test_ec2_client_raises_when_no_connection_available(ec2_client: EC2Client):
- with pytest.raises(
- botocore.exceptions.ClientError, match=r".+ AWS was not able to validate .+"
- ):
- await ec2_client.describe_account_attributes(DryRun=True)
-
-
-async def test_ec2_client_with_mock_server(
- mocked_aws_server_envs: None, ec2_client: EC2Client
-):
- # passes without exception
- await ec2_client.describe_account_attributes(DryRun=True)
+from simcore_service_autoscaling.core.errors import ConfigurationError
+from simcore_service_autoscaling.modules.ec2 import get_ec2_client
async def test_ec2_does_not_initialize_if_deactivated(
@@ -73,271 +15,6 @@ async def test_ec2_does_not_initialize_if_deactivated(
initialized_app: FastAPI,
):
assert hasattr(initialized_app.state, "ec2_client")
- assert initialized_app.state.ec2_client == None
+ assert initialized_app.state.ec2_client is None
with pytest.raises(ConfigurationError):
get_ec2_client(initialized_app)
-
-
-async def test_ec2_client_when_ec2_server_goes_up_and_down(
- mocked_aws_server: ThreadedMotoServer,
- mocked_aws_server_envs: None,
- ec2_client: EC2Client,
-):
- # passes without exception
- await ec2_client.describe_account_attributes(DryRun=True)
- mocked_aws_server.stop()
- with pytest.raises(botocore.exceptions.EndpointConnectionError):
- await ec2_client.describe_account_attributes(DryRun=True)
-
- # restart
- mocked_aws_server.start()
- # passes without exception
- await ec2_client.describe_account_attributes(DryRun=True)
-
-
-async def test_ping(
- mocked_aws_server: ThreadedMotoServer,
- mocked_aws_server_envs: None,
- aws_allowed_ec2_instance_type_names_env: list[str],
- app_settings: ApplicationSettings,
- autoscaling_ec2: AutoscalingEC2,
-):
- assert await autoscaling_ec2.ping() is True
- mocked_aws_server.stop()
- assert await autoscaling_ec2.ping() is False
- mocked_aws_server.start()
- assert await autoscaling_ec2.ping() is True
-
-
-async def test_get_ec2_instance_capabilities(
- mocked_aws_server_envs: None,
- aws_allowed_ec2_instance_type_names_env: list[str],
- app_settings: ApplicationSettings,
- autoscaling_ec2: AutoscalingEC2,
-):
- assert app_settings.AUTOSCALING_EC2_INSTANCES
- assert app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_ALLOWED_TYPES
- instance_types = await autoscaling_ec2.get_ec2_instance_capabilities(
- cast(
- set[InstanceTypeType],
- set(app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_ALLOWED_TYPES),
- )
- )
- assert instance_types
- assert len(instance_types) == len(
- app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_ALLOWED_TYPES
- )
-
- # all the instance names are found and valid
- assert all(
- i.name in app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_ALLOWED_TYPES
- for i in instance_types
- )
- for (
- instance_type_name
- ) in app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_ALLOWED_TYPES:
- assert any(i.name == instance_type_name for i in instance_types)
-
-
-@pytest.fixture
-async def fake_ec2_instance_type(
- mocked_aws_server_envs: None,
- ec2_client: EC2Client,
-) -> EC2InstanceType:
- instance_type_name: InstanceTypeType = parse_obj_as(InstanceTypeType, "c3.8xlarge")
- instance_types = await ec2_client.describe_instance_types(
- InstanceTypes=[instance_type_name]
- )
- assert instance_types
- assert "InstanceTypes" in instance_types
- assert instance_types["InstanceTypes"]
- assert "MemoryInfo" in instance_types["InstanceTypes"][0]
- assert "SizeInMiB" in instance_types["InstanceTypes"][0]["MemoryInfo"]
- assert "VCpuInfo" in instance_types["InstanceTypes"][0]
- assert "DefaultVCpus" in instance_types["InstanceTypes"][0]["VCpuInfo"]
-
- return EC2InstanceType(
- name=instance_type_name,
- cpus=instance_types["InstanceTypes"][0]["VCpuInfo"]["DefaultVCpus"],
- ram=parse_obj_as(
- ByteSize,
- f"{instance_types['InstanceTypes'][0]['MemoryInfo']['SizeInMiB']}MiB",
- ),
- )
-
-
-async def test_start_aws_instance(
- mocked_aws_server_envs: None,
- aws_vpc_id: str,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- ec2_client: EC2Client,
- autoscaling_ec2: AutoscalingEC2,
- app_settings: ApplicationSettings,
- faker: Faker,
- fake_ec2_instance_type: EC2InstanceType,
-):
- assert app_settings.AUTOSCALING_EC2_ACCESS
- assert app_settings.AUTOSCALING_EC2_INSTANCES
- # we have nothing running now in ec2
- all_instances = await ec2_client.describe_instances()
- assert not all_instances["Reservations"]
-
- tags = faker.pydict(allowed_types=(str,))
- startup_script = faker.pystr()
- await autoscaling_ec2.start_aws_instance(
- app_settings.AUTOSCALING_EC2_INSTANCES,
- fake_ec2_instance_type,
- tags=tags,
- startup_script=startup_script,
- number_of_instances=1,
- )
-
- # check we have that now in ec2
- all_instances = await ec2_client.describe_instances()
- assert len(all_instances["Reservations"]) == 1
- running_instance = all_instances["Reservations"][0]
- assert "Instances" in running_instance
- assert len(running_instance["Instances"]) == 1
- running_instance = running_instance["Instances"][0]
- assert "InstanceType" in running_instance
- assert running_instance["InstanceType"] == fake_ec2_instance_type.name
- assert "Tags" in running_instance
- assert running_instance["Tags"] == [
- {"Key": key, "Value": value} for key, value in tags.items()
- ]
-
-
-async def test_start_aws_instance_is_limited_in_number_of_instances(
- mocked_aws_server_envs: None,
- aws_vpc_id: str,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- ec2_client: EC2Client,
- autoscaling_ec2: AutoscalingEC2,
- app_settings: ApplicationSettings,
- faker: Faker,
- fake_ec2_instance_type: EC2InstanceType,
-):
- assert app_settings.AUTOSCALING_EC2_ACCESS
- assert app_settings.AUTOSCALING_EC2_INSTANCES
- # we have nothing running now in ec2
- all_instances = await ec2_client.describe_instances()
- assert not all_instances["Reservations"]
-
- # create as many instances as we can
- tags = faker.pydict(allowed_types=(str,))
- startup_script = faker.pystr()
- for _ in range(app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MAX_INSTANCES):
- await autoscaling_ec2.start_aws_instance(
- app_settings.AUTOSCALING_EC2_INSTANCES,
- fake_ec2_instance_type,
- tags=tags,
- startup_script=startup_script,
- number_of_instances=1,
- )
-
- # now creating one more shall fail
- with pytest.raises(Ec2TooManyInstancesError):
- await autoscaling_ec2.start_aws_instance(
- app_settings.AUTOSCALING_EC2_INSTANCES,
- fake_ec2_instance_type,
- tags=tags,
- startup_script=startup_script,
- number_of_instances=1,
- )
-
-
-async def test_get_instances(
- mocked_aws_server_envs: None,
- aws_vpc_id: str,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- ec2_client: EC2Client,
- autoscaling_ec2: AutoscalingEC2,
- app_settings: ApplicationSettings,
- faker: Faker,
- fake_ec2_instance_type: EC2InstanceType,
-):
- assert app_settings.AUTOSCALING_EC2_INSTANCES
- # we have nothing running now in ec2
- all_instances = await ec2_client.describe_instances()
- assert not all_instances["Reservations"]
- assert (
- await autoscaling_ec2.get_instances(app_settings.AUTOSCALING_EC2_INSTANCES, {})
- == []
- )
-
- # create some instance
- tags = faker.pydict(allowed_types=(str,))
- startup_script = faker.pystr()
- created_instances = await autoscaling_ec2.start_aws_instance(
- app_settings.AUTOSCALING_EC2_INSTANCES,
- fake_ec2_instance_type,
- tags=tags,
- startup_script=startup_script,
- number_of_instances=1,
- )
- assert len(created_instances) == 1
-
- instance_received = await autoscaling_ec2.get_instances(
- app_settings.AUTOSCALING_EC2_INSTANCES,
- tags=tags,
- )
- assert created_instances == instance_received
-
-
-async def test_terminate_instance(
- mocked_aws_server_envs: None,
- aws_vpc_id: str,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- ec2_client: EC2Client,
- autoscaling_ec2: AutoscalingEC2,
- app_settings: ApplicationSettings,
- faker: Faker,
- fake_ec2_instance_type: EC2InstanceType,
-):
- assert app_settings.AUTOSCALING_EC2_INSTANCES
- # we have nothing running now in ec2
- all_instances = await ec2_client.describe_instances()
- assert not all_instances["Reservations"]
- # create some instance
- tags = faker.pydict(allowed_types=(str,))
- startup_script = faker.pystr()
- created_instances = await autoscaling_ec2.start_aws_instance(
- app_settings.AUTOSCALING_EC2_INSTANCES,
- fake_ec2_instance_type,
- tags=tags,
- startup_script=startup_script,
- number_of_instances=1,
- )
- assert len(created_instances) == 1
-
- # terminate the instance
- await autoscaling_ec2.terminate_instances(created_instances)
- # calling it several times is ok, the instance stays a while
- await autoscaling_ec2.terminate_instances(created_instances)
-
-
-async def test_terminate_instance_not_existing_raises(
- mocked_aws_server_envs: None,
- aws_vpc_id: str,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- ec2_client: EC2Client,
- autoscaling_ec2: AutoscalingEC2,
- app_settings: ApplicationSettings,
- fake_ec2_instance_data: Callable[..., EC2InstanceData],
-):
- assert app_settings.AUTOSCALING_EC2_INSTANCES
- # we have nothing running now in ec2
- all_instances = await ec2_client.describe_instances()
- assert not all_instances["Reservations"]
- with pytest.raises(Ec2InstanceNotFoundError):
- await autoscaling_ec2.terminate_instances([fake_ec2_instance_data()])
diff --git a/services/autoscaling/tests/unit/test_utils_computational_scaling.py b/services/autoscaling/tests/unit/test_utils_computational_scaling.py
index 17c3e5f5d9d..945b883187e 100644
--- a/services/autoscaling/tests/unit/test_utils_computational_scaling.py
+++ b/services/autoscaling/tests/unit/test_utils_computational_scaling.py
@@ -8,6 +8,7 @@
from unittest import mock
import pytest
+from aws_library.ec2.models import EC2InstanceType, Resources
from faker import Faker
from models_library.generated_models.docker_rest_api import Node as DockerNode
from pydantic import ByteSize, parse_obj_as
@@ -17,8 +18,6 @@
DaskTask,
DaskTaskResources,
EC2InstanceData,
- EC2InstanceType,
- Resources,
)
from simcore_service_autoscaling.utils.computational_scaling import (
_DEFAULT_MAX_CPU,
diff --git a/services/autoscaling/tests/unit/test_utils_docker.py b/services/autoscaling/tests/unit/test_utils_docker.py
index 3a5985becf0..e9ee5c21c27 100644
--- a/services/autoscaling/tests/unit/test_utils_docker.py
+++ b/services/autoscaling/tests/unit/test_utils_docker.py
@@ -6,11 +6,13 @@
import datetime
import itertools
import random
+from collections.abc import AsyncIterator, Awaitable, Callable
from copy import deepcopy
-from typing import Any, AsyncIterator, Awaitable, Callable
+from typing import Any
import aiodocker
import pytest
+from aws_library.ec2.models import Resources
from deepdiff import DeepDiff
from faker import Faker
from models_library.docker import DockerGenericTag, DockerLabelKey
@@ -24,7 +26,6 @@
from pydantic import ByteSize, parse_obj_as
from pytest_mock.plugin import MockerFixture
from servicelib.docker_utils import to_datetime
-from simcore_service_autoscaling.models import Resources
from simcore_service_autoscaling.modules.docker import AutoscalingDocker
from simcore_service_autoscaling.utils.utils_docker import (
Node,
@@ -72,7 +73,6 @@ async def _creator(labels: list[str]) -> None:
"Labels": {f"{label}": "true" for label in labels},
},
)
- return
yield _creator
# revert labels
@@ -145,14 +145,14 @@ async def test_worker_nodes(
async def test_remove_monitored_down_nodes_with_empty_list_does_nothing(
autoscaling_docker: AutoscalingDocker,
):
- assert await remove_nodes(autoscaling_docker, []) == []
+ assert await remove_nodes(autoscaling_docker, nodes=[]) == []
async def test_remove_monitored_down_nodes_of_non_down_node_does_nothing(
autoscaling_docker: AutoscalingDocker,
host_node: Node,
):
- assert await remove_nodes(autoscaling_docker, [host_node]) == []
+ assert await remove_nodes(autoscaling_docker, nodes=[host_node]) == []
@pytest.fixture
@@ -174,7 +174,7 @@ async def test_remove_monitored_down_nodes_of_down_node(
assert fake_docker_node.Status
fake_docker_node.Status.State = NodeState.down
assert fake_docker_node.Status.State == NodeState.down
- assert await remove_nodes(autoscaling_docker, [fake_docker_node]) == [
+ assert await remove_nodes(autoscaling_docker, nodes=[fake_docker_node]) == [
fake_docker_node
]
# NOTE: this is the same as calling with aiodocker.Docker() as docker: docker.nodes.remove()
@@ -190,7 +190,7 @@ async def test_remove_monitored_down_node_with_unexpected_state_does_nothing(
assert fake_docker_node.Status
fake_docker_node.Status = None
assert not fake_docker_node.Status
- assert await remove_nodes(autoscaling_docker, [fake_docker_node]) == []
+ assert await remove_nodes(autoscaling_docker, nodes=[fake_docker_node]) == []
async def test_pending_service_task_with_insufficient_resources_with_no_service(
diff --git a/services/autoscaling/tests/unit/test_utils_dynamic_scaling.py b/services/autoscaling/tests/unit/test_utils_dynamic_scaling.py
index ccc283144b3..d27bc0dd28f 100644
--- a/services/autoscaling/tests/unit/test_utils_dynamic_scaling.py
+++ b/services/autoscaling/tests/unit/test_utils_dynamic_scaling.py
@@ -8,12 +8,11 @@
from datetime import timedelta
import pytest
+from aws_library.ec2.models import EC2InstanceData, EC2InstanceType
from faker import Faker
from models_library.generated_models.docker_rest_api import Task
from pydantic import ByteSize
from pytest_mock import MockerFixture
-from simcore_service_autoscaling.models import EC2InstanceType
-from simcore_service_autoscaling.modules.ec2 import EC2InstanceData
from simcore_service_autoscaling.utils.dynamic_scaling import (
try_assigning_task_to_pending_instances,
)
diff --git a/services/autoscaling/tests/unit/test_utils_ec2.py b/services/autoscaling/tests/unit/test_utils_ec2.py
index dc9ea43d8be..8697c8d5f11 100644
--- a/services/autoscaling/tests/unit/test_utils_ec2.py
+++ b/services/autoscaling/tests/unit/test_utils_ec2.py
@@ -6,15 +6,14 @@
import random
import pytest
+from aws_library.ec2.models import EC2InstanceType, Resources
from faker import Faker
from pydantic import ByteSize
from simcore_service_autoscaling.core.errors import (
ConfigurationError,
Ec2InstanceNotFoundError,
)
-from simcore_service_autoscaling.models import Resources
from simcore_service_autoscaling.utils.utils_ec2 import (
- EC2InstanceType,
closest_instance_policy,
compose_user_data,
find_best_fitting_ec2_instance,
@@ -60,10 +59,11 @@ async def test_find_best_fitting_ec2_instance_closest_instance_policy_with_resou
[
(
Resources(cpus=n, ram=ByteSize(n)),
- EC2InstanceType(name="fake", cpus=n, ram=ByteSize(n)),
+ EC2InstanceType(name="c5ad.12xlarge", cpus=n, ram=ByteSize(n)),
)
for n in range(1, 30)
],
+ ids=str,
)
async def test_find_best_fitting_ec2_instance_closest_instance_policy(
needed_resources: Resources,
@@ -77,7 +77,7 @@ async def test_find_best_fitting_ec2_instance_closest_instance_policy(
)
SKIPPED_KEYS = ["name"]
- for k in found_instance.__dict__.keys():
+ for k in found_instance.__dict__:
if k not in SKIPPED_KEYS:
assert getattr(found_instance, k) == getattr(expected_ec2_instance, k)
diff --git a/services/clusters-keeper/requirements/_base.in b/services/clusters-keeper/requirements/_base.in
index 25eb0daeb8a..dc3b222d6db 100644
--- a/services/clusters-keeper/requirements/_base.in
+++ b/services/clusters-keeper/requirements/_base.in
@@ -9,14 +9,13 @@
# intra-repo required dependencies
--requirement ../../../packages/models-library/requirements/_base.in
--requirement ../../../packages/settings-library/requirements/_base.in
+--requirement ../../../packages/aws-library/requirements/_base.in
# service-library[fastapi]
--requirement ../../../packages/service-library/requirements/_base.in
--requirement ../../../packages/service-library/requirements/_fastapi.in
-aioboto3
dask[distributed]
fastapi
packaging
-types-aiobotocore[ec2]
diff --git a/services/clusters-keeper/requirements/_base.txt b/services/clusters-keeper/requirements/_base.txt
index 2202b2fea87..3436e8e8028 100644
--- a/services/clusters-keeper/requirements/_base.txt
+++ b/services/clusters-keeper/requirements/_base.txt
@@ -7,25 +7,39 @@
aio-pika==9.3.0
# via
# -c requirements/../../../packages/service-library/requirements/./_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/_base.in
aioboto3==12.0.0
- # via -r requirements/_base.in
+ # via -r requirements/../../../packages/aws-library/requirements/_base.in
aiobotocore==2.7.0
- # via aioboto3
+ # via
+ # aioboto3
+ # aiobotocore
+aiocache==0.12.2
+ # via -r requirements/../../../packages/aws-library/requirements/_base.in
aiodebug==2.3.0
# via
# -c requirements/../../../packages/service-library/requirements/./_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/_base.in
aiodocker==0.21.0
# via
# -c requirements/../../../packages/service-library/requirements/./_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/_base.in
aiofiles==23.2.1
# via
# -c requirements/../../../packages/service-library/requirements/./_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/_base.in
aiohttp==3.8.6
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -50,6 +64,9 @@ anyio==4.0.0
arrow==1.3.0
# via
# -c requirements/../../../packages/service-library/requirements/./_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in
# -r requirements/../../../packages/models-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/./../../../packages/models-library/requirements/_base.in
@@ -74,6 +91,12 @@ botocore-stubs==1.31.80
# via types-aiobotocore
certifi==2023.7.22
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -116,6 +139,12 @@ exceptiongroup==1.1.3
# via anyio
fastapi==0.99.1
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -143,6 +172,12 @@ httpcore==1.0.1
# via httpx
httpx==0.25.1
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -165,6 +200,12 @@ importlib-metadata==6.8.0
# dask
jinja2==3.1.2
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -182,6 +223,8 @@ jmespath==1.0.1
# botocore
jsonschema==4.19.2
# via
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
# -r requirements/../../../packages/models-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/./../../../packages/models-library/requirements/_base.in
@@ -210,6 +253,8 @@ multidict==6.0.4
# yarl
orjson==3.9.10
# via
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
# -r requirements/../../../packages/models-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/./../../../packages/models-library/requirements/_base.in
@@ -231,6 +276,12 @@ psutil==5.9.5
# distributed
pydantic==1.10.13
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -241,6 +292,12 @@ pydantic==1.10.13
# -c requirements/../../../packages/service-library/requirements/./_base.in
# -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../requirements/constraints.txt
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/_base.in
+ # -r requirements/../../../packages/aws-library/requirements/_base.in
# -r requirements/../../../packages/models-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in
@@ -254,6 +311,7 @@ pygments==2.16.1
pyinstrument==4.6.0
# via
# -c requirements/../../../packages/service-library/requirements/./_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/_base.in
python-dateutil==2.8.2
# via
@@ -261,6 +319,12 @@ python-dateutil==2.8.2
# botocore
pyyaml==6.0.1
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -272,11 +336,18 @@ pyyaml==6.0.1
# -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../requirements/constraints.txt
# -c requirements/../../../services/dask-sidecar/requirements/_dask-distributed.txt
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/_base.in
# dask
# distributed
redis==5.0.1
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -287,15 +358,19 @@ redis==5.0.1
# -c requirements/../../../packages/service-library/requirements/./_base.in
# -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../requirements/constraints.txt
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/_base.in
referencing==0.29.3
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/./constraints.txt
# -c requirements/../../../packages/service-library/requirements/././constraints.txt
# -c requirements/../../../packages/service-library/requirements/./constraints.txt
# jsonschema
# jsonschema-specifications
rich==13.6.0
# via
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/./../../../packages/settings-library/requirements/_base.in
# -r requirements/../../../packages/settings-library/requirements/_base.in
@@ -317,6 +392,12 @@ sortedcontainers==2.4.0
# distributed
starlette==0.27.0
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
@@ -334,11 +415,13 @@ tblib==2.0.0
tenacity==8.2.3
# via
# -c requirements/../../../packages/service-library/requirements/./_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/_base.in
toolz==0.12.0
# via
# -c requirements/../../../packages/service-library/requirements/./_base.in
# -c requirements/../../../services/dask-sidecar/requirements/_dask-distributed.txt
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/_base.in
# dask
# distributed
@@ -350,14 +433,17 @@ tornado==6.3.3
tqdm==4.66.1
# via
# -c requirements/../../../packages/service-library/requirements/./_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/_base.in
typer==0.9.0
# via
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in
+ # -r requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in
# -r requirements/../../../packages/service-library/requirements/./../../../packages/settings-library/requirements/_base.in
# -r requirements/../../../packages/settings-library/requirements/_base.in
types-aiobotocore==2.7.0
- # via -r requirements/_base.in
+ # via -r requirements/../../../packages/aws-library/requirements/_base.in
types-aiobotocore-ec2==2.7.0
# via types-aiobotocore
types-awscrt==0.19.8
@@ -376,6 +462,12 @@ typing-extensions==4.8.0
# uvicorn
urllib3==1.26.16
# via
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
+ # -c requirements/../../../packages/aws-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
diff --git a/services/clusters-keeper/requirements/_test.txt b/services/clusters-keeper/requirements/_test.txt
index 85dd0cf4dcc..411492a8d69 100644
--- a/services/clusters-keeper/requirements/_test.txt
+++ b/services/clusters-keeper/requirements/_test.txt
@@ -264,7 +264,9 @@ python-dateutil==2.8.2
python-dotenv==1.0.0
# via -r requirements/_test.in
python-jose==3.3.0
- # via moto
+ # via
+ # moto
+ # python-jose
pyyaml==6.0.1
# via
# -c requirements/../../../requirements/constraints.txt
diff --git a/services/clusters-keeper/requirements/ci.txt b/services/clusters-keeper/requirements/ci.txt
index 1a24f9bf9fe..972b44964e6 100644
--- a/services/clusters-keeper/requirements/ci.txt
+++ b/services/clusters-keeper/requirements/ci.txt
@@ -11,6 +11,7 @@
--requirement _test.txt
# installs this repo's packages
+../../packages/aws-library
../../packages/models-library
../../packages/pytest-simcore
../../packages/service-library[fastapi]
diff --git a/services/clusters-keeper/requirements/dev.txt b/services/clusters-keeper/requirements/dev.txt
index f8a28b5d52d..5324f4c79f7 100644
--- a/services/clusters-keeper/requirements/dev.txt
+++ b/services/clusters-keeper/requirements/dev.txt
@@ -12,6 +12,7 @@
--requirement _tools.txt
# installs this repo's packages
+--editable ../../packages/aws-library
--editable ../../packages/models-library
--editable ../../packages/pytest-simcore
--editable ../../packages/service-library[fastapi]
diff --git a/services/clusters-keeper/requirements/prod.txt b/services/clusters-keeper/requirements/prod.txt
index 8e90729c4fc..f009eafa289 100644
--- a/services/clusters-keeper/requirements/prod.txt
+++ b/services/clusters-keeper/requirements/prod.txt
@@ -10,6 +10,7 @@
--requirement _base.txt
# installs this repo's packages
+../../packages/aws-library
../../packages/models-library
../../packages/service-library[fastapi]
../../packages/settings-library
diff --git a/services/clusters-keeper/setup.py b/services/clusters-keeper/setup.py
index 63b4d8ed7c5..8a67e39e66d 100755
--- a/services/clusters-keeper/setup.py
+++ b/services/clusters-keeper/setup.py
@@ -30,6 +30,7 @@ def read_reqs(reqs_path: Path) -> set[str]:
PROD_REQUIREMENTS = tuple(
read_reqs(CURRENT_DIR / "requirements" / "_base.txt")
| {
+ "simcore-aws-library",
"simcore-models-library",
"simcore-service-library[fastapi]",
"simcore-settings-library",
diff --git a/services/clusters-keeper/src/simcore_service_clusters_keeper/core/settings.py b/services/clusters-keeper/src/simcore_service_clusters_keeper/core/settings.py
index 4b69f776900..491cdeec854 100644
--- a/services/clusters-keeper/src/simcore_service_clusters_keeper/core/settings.py
+++ b/services/clusters-keeper/src/simcore_service_clusters_keeper/core/settings.py
@@ -1,6 +1,6 @@
import datetime
from functools import cached_property
-from typing import cast
+from typing import Any, ClassVar, Final, cast
from fastapi import FastAPI
from models_library.basic_types import (
@@ -12,6 +12,7 @@
from pydantic import Field, NonNegativeInt, PositiveInt, parse_obj_as, validator
from settings_library.base import BaseCustomSettings
from settings_library.docker_registry import RegistrySettings
+from settings_library.ec2 import EC2Settings
from settings_library.rabbit import RabbitSettings
from settings_library.redis import RedisSettings
from settings_library.utils_logging import MixinLoggingSettings
@@ -19,14 +20,23 @@
from .._meta import API_VERSION, API_VTAG, APP_NAME
+CLUSTERS_KEEPER_ENV_PREFIX: Final[str] = "CLUSTERS_KEEPER_"
-class EC2ClustersKeeperSettings(BaseCustomSettings):
- EC2_CLUSTERS_KEEPER_ACCESS_KEY_ID: str
- EC2_CLUSTERS_KEEPER_ENDPOINT: str | None = Field(
- default=None, description="do not define if using standard AWS"
- )
- EC2_CLUSTERS_KEEPER_REGION_NAME: str = "us-east-1"
- EC2_CLUSTERS_KEEPER_SECRET_ACCESS_KEY: str
+
+class ClustersKeeperEC2Settings(EC2Settings):
+ class Config(EC2Settings.Config):
+ env_prefix = CLUSTERS_KEEPER_ENV_PREFIX
+
+ schema_extra: ClassVar[dict[str, Any]] = {
+ "examples": [
+ {
+ f"{CLUSTERS_KEEPER_ENV_PREFIX}EC2_ACCESS_KEY_ID": "my_access_key_id",
+ f"{CLUSTERS_KEEPER_ENV_PREFIX}EC2_ENDPOINT": "http://my_ec2_endpoint.com",
+ f"{CLUSTERS_KEEPER_ENV_PREFIX}EC2_REGION_NAME": "us-east-1",
+ f"{CLUSTERS_KEEPER_ENV_PREFIX}EC2_SECRET_ACCESS_KEY": "my_secret_access_key",
+ }
+ ],
+ }
class WorkersEC2InstancesSettings(BaseCustomSettings):
@@ -181,7 +191,7 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings):
description="Enables local development log format. WARNING: make sure it is disabled if you want to have structured logs!",
)
- CLUSTERS_KEEPER_EC2_ACCESS: EC2ClustersKeeperSettings | None = Field(
+ CLUSTERS_KEEPER_EC2_ACCESS: ClustersKeeperEC2Settings | None = Field(
auto_default_from_env=True
)
diff --git a/services/clusters-keeper/src/simcore_service_clusters_keeper/models.py b/services/clusters-keeper/src/simcore_service_clusters_keeper/models.py
deleted file mode 100644
index 6217940b44d..00000000000
--- a/services/clusters-keeper/src/simcore_service_clusters_keeper/models.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import datetime
-from dataclasses import dataclass
-from typing import TypeAlias
-
-from types_aiobotocore_ec2.literals import InstanceStateNameType, InstanceTypeType
-
-InstancePrivateDNSName = str
-EC2Tags: TypeAlias = dict[str, str]
-
-
-@dataclass(frozen=True)
-class EC2InstanceData:
- launch_time: datetime.datetime
- id: str # noqa: A003
- aws_private_dns: InstancePrivateDNSName
- aws_public_ip: str | None
- type: InstanceTypeType # noqa: A003
- state: InstanceStateNameType
- tags: EC2Tags
diff --git a/services/clusters-keeper/src/simcore_service_clusters_keeper/modules/clusters.py b/services/clusters-keeper/src/simcore_service_clusters_keeper/modules/clusters.py
index 895db73fdc8..a3a5a7229aa 100644
--- a/services/clusters-keeper/src/simcore_service_clusters_keeper/modules/clusters.py
+++ b/services/clusters-keeper/src/simcore_service_clusters_keeper/modules/clusters.py
@@ -2,6 +2,7 @@
import logging
from typing import cast
+from aws_library.ec2.models import EC2InstanceConfig, EC2InstanceData, EC2InstanceType
from fastapi import FastAPI
from models_library.users import UserID
from models_library.wallets import WalletID
@@ -10,7 +11,6 @@
from ..core.errors import Ec2InstanceNotFoundError
from ..core.settings import get_application_settings
-from ..models import EC2InstanceData
from ..utils.clusters import create_startup_script
from ..utils.ec2 import (
HEARTBEAT_TAG_KEY,
@@ -30,16 +30,23 @@ async def create_cluster(
ec2_client = get_ec2_client(app)
app_settings = get_application_settings(app)
assert app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES # nosec
- return await ec2_client.start_aws_instance(
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES,
- instance_type=cast(
- InstanceTypeType,
- next(
- iter(
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_ALLOWED_TYPES
- )
- ),
- ),
+ ec2_instance_types: list[
+ EC2InstanceType
+ ] = await ec2_client.get_ec2_instance_capabilities(
+ instance_type_names={
+ cast(
+ InstanceTypeType,
+ next(
+ iter(
+ app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_ALLOWED_TYPES
+ )
+ ),
+ )
+ }
+ )
+ assert len(ec2_instance_types) == 1 # nosec
+ instance_config = EC2InstanceConfig(
+ type=ec2_instance_types[0],
tags=creation_ec2_tags(app_settings, user_id=user_id, wallet_id=wallet_id),
startup_script=create_startup_script(
app_settings,
@@ -47,20 +54,30 @@ async def create_cluster(
app_settings, user_id=user_id, wallet_id=wallet_id, is_manager=False
),
),
+ ami_id=app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_AMI_ID,
+ key_name=app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_KEY_NAME,
+ security_group_ids=app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_SECURITY_GROUP_IDS,
+ subnet_id=app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_SUBNET_ID,
+ )
+ new_ec2_instance_data: list[EC2InstanceData] = await ec2_client.start_aws_instance(
+ instance_config,
number_of_instances=1,
+ max_number_of_instances=app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_MAX_INSTANCES,
)
+ return new_ec2_instance_data
async def get_all_clusters(app: FastAPI) -> list[EC2InstanceData]:
app_settings = get_application_settings(app)
assert app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES # nosec
- return await get_ec2_client(app).get_instances(
+ ec2_instance_data: list[EC2InstanceData] = await get_ec2_client(app).get_instances(
key_names=[
app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_KEY_NAME
],
tags=all_created_ec2_instances_filter(app_settings),
state_names=["running"],
)
+ return ec2_instance_data
async def get_cluster(
@@ -86,7 +103,7 @@ async def get_cluster_workers(
) -> list[EC2InstanceData]:
app_settings = get_application_settings(app)
assert app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES # nosec
- return await get_ec2_client(app).get_instances(
+ ec2_instance_data: list[EC2InstanceData] = await get_ec2_client(app).get_instances(
key_names=[
app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_KEY_NAME
],
@@ -94,6 +111,7 @@ async def get_cluster_workers(
"Name": f"{get_cluster_name(app_settings, user_id=user_id, wallet_id=wallet_id, is_manager=False)}*"
},
)
+ return ec2_instance_data
async def cluster_heartbeat(
diff --git a/services/clusters-keeper/src/simcore_service_clusters_keeper/modules/clusters_management_core.py b/services/clusters-keeper/src/simcore_service_clusters_keeper/modules/clusters_management_core.py
index 81fb41ff967..ce02d45166c 100644
--- a/services/clusters-keeper/src/simcore_service_clusters_keeper/modules/clusters_management_core.py
+++ b/services/clusters-keeper/src/simcore_service_clusters_keeper/modules/clusters_management_core.py
@@ -2,12 +2,12 @@
import logging
import arrow
+from aws_library.ec2.models import EC2InstanceData
from fastapi import FastAPI
from models_library.users import UserID
from models_library.wallets import WalletID
from ..core.settings import get_application_settings
-from ..models import EC2InstanceData
from ..modules.clusters import (
delete_clusters,
get_all_clusters,
@@ -25,7 +25,8 @@ def _get_instance_last_heartbeat(instance: EC2InstanceData) -> datetime.datetime
if last_heartbeat := instance.tags.get(HEARTBEAT_TAG_KEY, None):
last_heartbeat_time: datetime.datetime = arrow.get(last_heartbeat).datetime
return last_heartbeat_time
- return instance.launch_time
+ launch_time: datetime.datetime = instance.launch_time
+ return launch_time
async def _find_terminateable_instances(
diff --git a/services/clusters-keeper/src/simcore_service_clusters_keeper/modules/ec2.py b/services/clusters-keeper/src/simcore_service_clusters_keeper/modules/ec2.py
index 6a66adf6f00..bd74dc85f56 100644
--- a/services/clusters-keeper/src/simcore_service_clusters_keeper/modules/ec2.py
+++ b/services/clusters-keeper/src/simcore_service_clusters_keeper/modules/ec2.py
@@ -1,293 +1,25 @@
-import contextlib
import logging
-from dataclasses import dataclass
from typing import cast
-import aioboto3
-import botocore.exceptions
-from aiobotocore.session import ClientCreatorContext
+from aws_library.ec2.client import SimcoreEC2API
from fastapi import FastAPI
-from models_library.api_schemas_clusters_keeper.ec2_instances import EC2InstanceType
-from pydantic import ByteSize
-from servicelib.logging_utils import log_context
+from settings_library.ec2 import EC2Settings
from tenacity._asyncio import AsyncRetrying
from tenacity.before_sleep import before_sleep_log
from tenacity.stop import stop_after_delay
from tenacity.wait import wait_random_exponential
-from types_aiobotocore_ec2 import EC2Client
-from types_aiobotocore_ec2.literals import InstanceStateNameType, InstanceTypeType
-from types_aiobotocore_ec2.type_defs import FilterTypeDef
-from ..core.errors import (
- ClustersKeeperRuntimeError,
- ConfigurationError,
- Ec2InstanceNotFoundError,
- Ec2InstanceTypeInvalidError,
- Ec2NotConnectedError,
- Ec2TooManyInstancesError,
-)
-from ..core.settings import (
- EC2ClustersKeeperSettings,
- PrimaryEC2InstancesSettings,
- get_application_settings,
-)
-from ..models import EC2InstanceData, EC2Tags
-from ..utils.ec2 import compose_user_data
+from ..core.errors import ConfigurationError, Ec2NotConnectedError
+from ..core.settings import get_application_settings
logger = logging.getLogger(__name__)
-@dataclass(frozen=True)
-class ClustersKeeperEC2:
- client: EC2Client
- session: aioboto3.Session
- exit_stack: contextlib.AsyncExitStack
-
- @classmethod
- async def create(cls, settings: EC2ClustersKeeperSettings) -> "ClustersKeeperEC2":
- session = aioboto3.Session()
- session_client = session.client(
- "ec2",
- endpoint_url=settings.EC2_CLUSTERS_KEEPER_ENDPOINT,
- aws_access_key_id=settings.EC2_CLUSTERS_KEEPER_ACCESS_KEY_ID,
- aws_secret_access_key=settings.EC2_CLUSTERS_KEEPER_SECRET_ACCESS_KEY,
- region_name=settings.EC2_CLUSTERS_KEEPER_REGION_NAME,
- )
- assert isinstance(session_client, ClientCreatorContext) # nosec
- exit_stack = contextlib.AsyncExitStack()
- ec2_client = cast(
- EC2Client, await exit_stack.enter_async_context(session_client)
- )
- return cls(ec2_client, session, exit_stack)
-
- async def close(self) -> None:
- await self.exit_stack.aclose()
-
- async def ping(self) -> bool:
- try:
- await self.client.describe_account_attributes()
- return True
- except Exception: # pylint: disable=broad-except
- return False
-
- async def get_ec2_instance_capabilities(
- self,
- instance_type_names: set[InstanceTypeType],
- ) -> list[EC2InstanceType]:
- """returns the ec2 instance types from a list of instance type names
- NOTE: the order might differ!
- Arguments:
- instance_type_names -- the types to filter with
-
- Raises:
- Ec2InstanceTypeInvalidError: some invalid types were used as filter
- ClustersKeeperRuntimeError: unexpected error communicating with EC2
-
- """
- try:
- instance_types = await self.client.describe_instance_types(
- InstanceTypes=list(instance_type_names)
- )
- list_instances: list[EC2InstanceType] = []
- for instance in instance_types.get("InstanceTypes", []):
- with contextlib.suppress(KeyError):
- list_instances.append(
- EC2InstanceType(
- name=instance["InstanceType"],
- cpus=instance["VCpuInfo"]["DefaultVCpus"],
- ram=ByteSize(
- int(instance["MemoryInfo"]["SizeInMiB"]) * 1024 * 1024
- ),
- )
- )
- return list_instances
- except botocore.exceptions.ClientError as exc:
- if exc.response.get("Error", {}).get("Code", "") == "InvalidInstanceType":
- raise Ec2InstanceTypeInvalidError from exc
- raise ClustersKeeperRuntimeError from exc # pragma: no cover
-
- async def start_aws_instance(
- self,
- instance_settings: PrimaryEC2InstancesSettings,
- instance_type: InstanceTypeType,
- tags: EC2Tags,
- startup_script: str,
- number_of_instances: int,
- ) -> list[EC2InstanceData]:
- with log_context(
- logger,
- logging.INFO,
- msg=f"launching {number_of_instances} AWS instance(s) {instance_type} with {tags=}",
- ):
- # first check the max amount is not already reached
- current_instances = await self.get_instances(
- key_names=[instance_settings.PRIMARY_EC2_INSTANCES_KEY_NAME], tags=tags
- )
- if (
- len(current_instances) + number_of_instances
- > instance_settings.PRIMARY_EC2_INSTANCES_MAX_INSTANCES
- ):
- raise Ec2TooManyInstancesError(
- num_instances=instance_settings.PRIMARY_EC2_INSTANCES_MAX_INSTANCES
- )
-
- instances = await self.client.run_instances(
- ImageId=instance_settings.PRIMARY_EC2_INSTANCES_AMI_ID,
- MinCount=number_of_instances,
- MaxCount=number_of_instances,
- InstanceType=instance_type,
- InstanceInitiatedShutdownBehavior="terminate",
- KeyName=instance_settings.PRIMARY_EC2_INSTANCES_KEY_NAME,
- TagSpecifications=[
- {
- "ResourceType": "instance",
- "Tags": [
- {"Key": tag_key, "Value": tag_value}
- for tag_key, tag_value in tags.items()
- ],
- }
- ],
- UserData=compose_user_data(startup_script),
- NetworkInterfaces=[
- {
- "AssociatePublicIpAddress": True,
- "DeviceIndex": 0,
- "SubnetId": instance_settings.PRIMARY_EC2_INSTANCES_SUBNET_ID,
- "Groups": instance_settings.PRIMARY_EC2_INSTANCES_SECURITY_GROUP_IDS,
- }
- ],
- )
- instance_ids = [i["InstanceId"] for i in instances["Instances"]]
- logger.info(
- "New instances launched: %s, waiting for them to start now...",
- instance_ids,
- )
-
- # wait for the instance to be in a pending state
- # NOTE: reference to EC2 states https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html
- waiter = self.client.get_waiter("instance_exists")
- await waiter.wait(InstanceIds=instance_ids)
- logger.info("instances %s exists now.", instance_ids)
-
- # get the private IPs
- instances = await self.client.describe_instances(InstanceIds=instance_ids)
- instance_datas = [
- EC2InstanceData(
- launch_time=instance["LaunchTime"],
- id=instance["InstanceId"],
- aws_private_dns=instance["PrivateDnsName"],
- aws_public_ip=instance["PublicIpAddress"]
- if "PublicIpAddress" in instance
- else None,
- type=instance["InstanceType"],
- state=instance["State"]["Name"],
- tags={tag["Key"]: tag["Value"] for tag in instance["Tags"]},
- )
- for instance in instances["Reservations"][0]["Instances"]
- ]
- logger.info(
- "%s is available, happy computing!!",
- f"{instance_datas=}",
- )
- return instance_datas
-
- async def get_instances(
- self,
- *,
- key_names: list[str],
- tags: EC2Tags,
- state_names: list[InstanceStateNameType] | None = None,
- ) -> list[EC2InstanceData]:
- # NOTE: be careful: Name=instance-state-name,Values=["pending", "running"] means pending OR running
- # NOTE2: AND is done by repeating Name=instance-state-name,Values=pending Name=instance-state-name,Values=running
- if state_names is None:
- state_names = ["pending", "running"]
-
- filters: list[FilterTypeDef] = [
- {
- "Name": "key-name",
- "Values": key_names,
- },
- {"Name": "instance-state-name", "Values": state_names},
- ]
- filters.extend(
- [{"Name": f"tag:{key}", "Values": [value]} for key, value in tags.items()]
- )
-
- instances = await self.client.describe_instances(Filters=filters)
- all_instances = []
- for reservation in instances["Reservations"]:
- assert "Instances" in reservation # nosec
- for instance in reservation["Instances"]:
- assert "LaunchTime" in instance # nosec
- assert "InstanceId" in instance # nosec
- assert "PrivateDnsName" in instance # nosec
- assert "InstanceType" in instance # nosec
- assert "State" in instance # nosec
- assert "Name" in instance["State"] # nosec
- assert "Tags" in instance # nosec
- all_instances.append(
- EC2InstanceData(
- launch_time=instance["LaunchTime"],
- id=instance["InstanceId"],
- aws_private_dns=instance["PrivateDnsName"],
- aws_public_ip=instance["PublicIpAddress"]
- if "PublicIpAddress" in instance
- else None,
- type=instance["InstanceType"],
- state=instance["State"]["Name"],
- tags={
- tag["Key"]: tag["Value"]
- for tag in instance["Tags"]
- if all(k in tag for k in ["Key", "Value"])
- },
- )
- )
- logger.debug(
- "received: %s instances with %s", f"{len(all_instances)}", f"{state_names=}"
- )
- return all_instances
-
- async def terminate_instances(self, instance_datas: list[EC2InstanceData]) -> None:
- try:
- with log_context(
- logger,
- logging.INFO,
- msg=f"terminating instances {[i.id for i in instance_datas]}",
- ):
- await self.client.terminate_instances(
- InstanceIds=[i.id for i in instance_datas]
- )
- except botocore.exceptions.ClientError as exc:
- if (
- exc.response.get("Error", {}).get("Code", "")
- == "InvalidInstanceID.NotFound"
- ):
- raise Ec2InstanceNotFoundError from exc
- raise # pragma: no cover
-
- async def set_instances_tags(
- self, instances: list[EC2InstanceData], *, tags: EC2Tags
- ) -> None:
- with log_context(
- logger,
- logging.DEBUG,
- msg=f"setting {tags=} on instances '[{[i.id for i in instances]}]'",
- ):
- await self.client.create_tags(
- Resources=[i.id for i in instances],
- Tags=[
- {"Key": tag_key, "Value": tag_value}
- for tag_key, tag_value in tags.items()
- ],
- )
-
-
def setup(app: FastAPI) -> None:
async def on_startup() -> None:
app.state.ec2_client = None
- settings: EC2ClustersKeeperSettings | None = get_application_settings(
+ settings: EC2Settings | None = get_application_settings(
app
).CLUSTERS_KEEPER_EC2_ACCESS
@@ -295,7 +27,7 @@ async def on_startup() -> None:
logger.warning("EC2 client is de-activated in the settings")
return
- app.state.ec2_client = client = await ClustersKeeperEC2.create(settings)
+ app.state.ec2_client = client = await SimcoreEC2API.create(settings)
async for attempt in AsyncRetrying(
reraise=True,
@@ -310,15 +42,15 @@ async def on_startup() -> None:
async def on_shutdown() -> None:
if app.state.ec2_client:
- await cast(ClustersKeeperEC2, app.state.ec2_client).close()
+ await cast(SimcoreEC2API, app.state.ec2_client).close()
app.add_event_handler("startup", on_startup)
app.add_event_handler("shutdown", on_shutdown)
-def get_ec2_client(app: FastAPI) -> ClustersKeeperEC2:
+def get_ec2_client(app: FastAPI) -> SimcoreEC2API:
if not app.state.ec2_client:
raise ConfigurationError(
msg="EC2 client is not available. Please check the configuration."
)
- return cast(ClustersKeeperEC2, app.state.ec2_client)
+ return cast(SimcoreEC2API, app.state.ec2_client)
diff --git a/services/clusters-keeper/src/simcore_service_clusters_keeper/rpc/clusters.py b/services/clusters-keeper/src/simcore_service_clusters_keeper/rpc/clusters.py
index babb123e210..ce6ab0b54c1 100644
--- a/services/clusters-keeper/src/simcore_service_clusters_keeper/rpc/clusters.py
+++ b/services/clusters-keeper/src/simcore_service_clusters_keeper/rpc/clusters.py
@@ -1,3 +1,4 @@
+from aws_library.ec2.models import EC2InstanceData
from fastapi import FastAPI
from models_library.api_schemas_clusters_keeper.clusters import OnDemandCluster
from models_library.users import UserID
@@ -5,7 +6,6 @@
from servicelib.rabbitmq import RPCRouter
from ..core.errors import Ec2InstanceNotFoundError
-from ..models import EC2InstanceData
from ..modules import clusters
from ..modules.dask import ping_scheduler
from ..modules.redis import get_redis_client
diff --git a/services/clusters-keeper/src/simcore_service_clusters_keeper/rpc/ec2_instances.py b/services/clusters-keeper/src/simcore_service_clusters_keeper/rpc/ec2_instances.py
index 758564cf9dc..a68760ad762 100644
--- a/services/clusters-keeper/src/simcore_service_clusters_keeper/rpc/ec2_instances.py
+++ b/services/clusters-keeper/src/simcore_service_clusters_keeper/rpc/ec2_instances.py
@@ -1,5 +1,8 @@
+from dataclasses import asdict
+
+from aws_library.ec2.models import EC2InstanceType
from fastapi import FastAPI
-from models_library.api_schemas_clusters_keeper.ec2_instances import EC2InstanceType
+from models_library.api_schemas_clusters_keeper.ec2_instances import EC2InstanceTypeGet
from servicelib.rabbitmq import RPCRouter
from ..modules.ec2 import get_ec2_client
@@ -10,5 +13,8 @@
@router.expose()
async def get_instance_type_details(
app: FastAPI, *, instance_type_names: set[str]
-) -> list[EC2InstanceType]:
- return await get_ec2_client(app).get_ec2_instance_capabilities(instance_type_names)
+) -> list[EC2InstanceTypeGet]:
+ instance_capabilities: list[EC2InstanceType] = await get_ec2_client(
+ app
+ ).get_ec2_instance_capabilities(instance_type_names)
+ return [EC2InstanceTypeGet(**asdict(t)) for t in instance_capabilities]
diff --git a/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/clusters.py b/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/clusters.py
index f139a36dc19..64cdf4fcb10 100644
--- a/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/clusters.py
+++ b/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/clusters.py
@@ -3,6 +3,7 @@
import functools
from typing import Any, Final
+from aws_library.ec2.models import EC2InstanceData
from models_library.api_schemas_clusters_keeper.clusters import (
ClusterState,
OnDemandCluster,
@@ -14,7 +15,6 @@
from .._meta import PACKAGE_DATA_FOLDER
from ..core.settings import ApplicationSettings
-from ..models import EC2InstanceData
from .dask import get_scheduler_url
_DOCKER_COMPOSE_FILE_NAME: Final[str] = "docker-compose.yml"
@@ -40,8 +40,8 @@ def _convert_to_env_list(entries: list[Any]) -> str:
environment_variables = [
f"DOCKER_IMAGE_TAG={app_settings.CLUSTERS_KEEPER_COMPUTATIONAL_BACKEND_DOCKER_IMAGE_TAG}",
- f"EC2_CLUSTERS_KEEPER_ACCESS_KEY_ID={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_CLUSTERS_KEEPER_ACCESS_KEY_ID}",
- f"EC2_CLUSTERS_KEEPER_ENDPOINT={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_CLUSTERS_KEEPER_ENDPOINT}",
+ f"EC2_CLUSTERS_KEEPER_ACCESS_KEY_ID={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_ACCESS_KEY_ID}",
+ f"EC2_CLUSTERS_KEEPER_ENDPOINT={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_ENDPOINT}",
f"WORKERS_EC2_INSTANCES_ALLOWED_TYPES={_convert_to_env_list(app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_ALLOWED_TYPES)}",
f"WORKERS_EC2_INSTANCES_AMI_ID={app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_AMI_ID}",
f"WORKERS_EC2_INSTANCES_CUSTOM_BOOT_SCRIPTS={_convert_to_env_list(app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_CUSTOM_BOOT_SCRIPTS)}",
@@ -52,8 +52,8 @@ def _convert_to_env_list(entries: list[Any]) -> str:
f"WORKERS_EC2_INSTANCES_SECURITY_GROUP_IDS={_convert_to_env_list(app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_SECURITY_GROUP_IDS)}",
f"WORKERS_EC2_INSTANCES_SUBNET_ID={app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_SUBNET_ID}",
f"WORKERS_EC2_INSTANCES_TIME_BEFORE_TERMINATION={app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_TIME_BEFORE_TERMINATION}",
- f"EC2_CLUSTERS_KEEPER_REGION_NAME={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_CLUSTERS_KEEPER_REGION_NAME}",
- f"EC2_CLUSTERS_KEEPER_SECRET_ACCESS_KEY={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_CLUSTERS_KEEPER_SECRET_ACCESS_KEY}",
+ f"EC2_CLUSTERS_KEEPER_REGION_NAME={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_REGION_NAME}",
+ f"EC2_CLUSTERS_KEEPER_SECRET_ACCESS_KEY={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_SECRET_ACCESS_KEY}",
f"LOG_LEVEL={app_settings.LOG_LEVEL}",
]
diff --git a/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/dask.py b/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/dask.py
index 3db6435c820..ac5bb9400de 100644
--- a/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/dask.py
+++ b/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/dask.py
@@ -1,7 +1,6 @@
+from aws_library.ec2.models import EC2InstanceData
from pydantic import AnyUrl, parse_obj_as
-from ..models import EC2InstanceData
-
def get_scheduler_url(ec2_instance: EC2InstanceData) -> AnyUrl:
url: AnyUrl = parse_obj_as(AnyUrl, f"tcp://{ec2_instance.aws_public_ip}:8786")
diff --git a/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/ec2.py b/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/ec2.py
index 7b99b23f6c3..5774f42e0e4 100644
--- a/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/ec2.py
+++ b/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/ec2.py
@@ -1,12 +1,12 @@
from textwrap import dedent
from typing import Final
+from aws_library.ec2.models import EC2Tags
from models_library.users import UserID
from models_library.wallets import WalletID
from .._meta import VERSION
from ..core.settings import ApplicationSettings
-from ..models import EC2Tags
_APPLICATION_TAG_KEY: Final[str] = "io.simcore.clusters-keeper"
_APPLICATION_VERSION_TAG: Final[EC2Tags] = {
diff --git a/services/clusters-keeper/tests/unit/conftest.py b/services/clusters-keeper/tests/unit/conftest.py
index 8fcb73ceff0..26c97e27084 100644
--- a/services/clusters-keeper/tests/unit/conftest.py
+++ b/services/clusters-keeper/tests/unit/conftest.py
@@ -4,46 +4,40 @@
import importlib.resources
import json
-import random
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
-from datetime import timezone
from pathlib import Path
from typing import Any
import aiodocker
import httpx
import pytest
-import requests
import simcore_service_clusters_keeper
import simcore_service_clusters_keeper.data
import yaml
-from aiohttp.test_utils import unused_port
from asgi_lifespan import LifespanManager
+from aws_library.ec2.client import SimcoreEC2API
from faker import Faker
from fakeredis.aioredis import FakeRedis
from fastapi import FastAPI
from models_library.users import UserID
from models_library.wallets import WalletID
-from moto.server import ThreadedMotoServer
from pytest_mock.plugin import MockerFixture
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
from pytest_simcore.helpers.utils_envs import EnvVarsDict, setenvs_from_dict
from servicelib.rabbitmq import RabbitMQRPCClient
+from settings_library.ec2 import EC2Settings
from settings_library.rabbit import RabbitSettings
from simcore_service_clusters_keeper.core.application import create_app
from simcore_service_clusters_keeper.core.settings import (
+ CLUSTERS_KEEPER_ENV_PREFIX,
ApplicationSettings,
- EC2ClustersKeeperSettings,
-)
-from simcore_service_clusters_keeper.modules.ec2 import (
- ClustersKeeperEC2,
- EC2InstanceData,
)
from simcore_service_clusters_keeper.utils.ec2 import get_cluster_name
from types_aiobotocore_ec2.client import EC2Client
from types_aiobotocore_ec2.literals import InstanceTypeType
pytest_plugins = [
+ "pytest_simcore.aws_server",
+ "pytest_simcore.aws_ec2_service",
"pytest_simcore.dask_scheduler",
"pytest_simcore.docker_compose",
"pytest_simcore.docker_swarm",
@@ -76,6 +70,19 @@ def ec2_instances() -> list[InstanceTypeType]:
return ["t2.nano", "m5.12xlarge"]
+@pytest.fixture
+def mocked_ec2_server_envs(
+ mocked_ec2_server_settings: EC2Settings,
+ monkeypatch: pytest.MonkeyPatch,
+) -> EnvVarsDict:
+ # NOTE: overrides the EC2Settings with what clusters-keeper expects
+ changed_envs: EnvVarsDict = {
+ f"{CLUSTERS_KEEPER_ENV_PREFIX}{k}": v
+ for k, v in mocked_ec2_server_settings.dict().items()
+ }
+ return setenvs_from_dict(monkeypatch, changed_envs)
+
+
@pytest.fixture
def app_environment(
mock_env_devel_environment: EnvVarsDict,
@@ -88,8 +95,8 @@ def app_environment(
monkeypatch,
{
"CLUSTERS_KEEPER_EC2_ACCESS": "{}",
- "EC2_CLUSTERS_KEEPER_ACCESS_KEY_ID": faker.pystr(),
- "EC2_CLUSTERS_KEEPER_SECRET_ACCESS_KEY": faker.pystr(),
+ "CLUSTERS_KEEPER_EC2_ACCESS_KEY_ID": faker.pystr(),
+ "CLUSTERS_KEEPER_EC2_SECRET_ACCESS_KEY": faker.pystr(),
"CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES": "{}",
"PRIMARY_EC2_INSTANCES_KEY_NAME": faker.pystr(),
"PRIMARY_EC2_INSTANCES_SECURITY_GROUP_IDS": json.dumps(
@@ -111,6 +118,28 @@ def app_environment(
return mock_env_devel_environment | envs
+@pytest.fixture
+def mocked_primary_ec2_instances_envs(
+ app_environment: EnvVarsDict,
+ monkeypatch: pytest.MonkeyPatch,
+ aws_security_group_id: str,
+ aws_subnet_id: str,
+ aws_ami_id: str,
+) -> EnvVarsDict:
+ envs = setenvs_from_dict(
+ monkeypatch,
+ {
+ "PRIMARY_EC2_INSTANCES_KEY_NAME": "osparc-pytest",
+ "PRIMARY_EC2_INSTANCES_SECURITY_GROUP_IDS": json.dumps(
+ [aws_security_group_id]
+ ),
+ "PRIMARY_EC2_INSTANCES_SUBNET_ID": aws_subnet_id,
+ "PRIMARY_EC2_INSTANCES_AMI_ID": aws_ami_id,
+ },
+ )
+ return app_environment | envs
+
+
@pytest.fixture
def disable_clusters_management_background_task(
mocker: MockerFixture,
@@ -175,53 +204,6 @@ async def async_client(initialized_app: FastAPI) -> AsyncIterator[httpx.AsyncCli
yield client
-@pytest.fixture(scope="module")
-def mocked_aws_server() -> Iterator[ThreadedMotoServer]:
- """creates a moto-server that emulates AWS services in place
- NOTE: Never use a bucket with underscores it fails!!
- """
- server = ThreadedMotoServer(ip_address=get_localhost_ip(), port=unused_port())
- # pylint: disable=protected-access
- print(
- f"--> started mock AWS server on {server._ip_address}:{server._port}" # noqa: SLF001
- )
- print(
- f"--> Dashboard available on [http://{server._ip_address}:{server._port}/moto-api/]" # noqa: SLF001
- )
- server.start()
- yield server
- server.stop()
- print(
- f"<-- stopped mock AWS server on {server._ip_address}:{server._port}" # noqa: SLF001
- )
-
-
-@pytest.fixture
-def reset_aws_server_state(mocked_aws_server: ThreadedMotoServer) -> Iterator[None]:
- # NOTE: reset_aws_server_state [http://docs.getmoto.org/en/latest/docs/server_mode.html#reset-api]
- yield
- # pylint: disable=protected-access
- requests.post(
- f"http://{mocked_aws_server._ip_address}:{mocked_aws_server._port}/moto-api/reset", # noqa: SLF001
- timeout=10,
- )
-
-
-@pytest.fixture
-def mocked_aws_server_envs(
- app_environment: EnvVarsDict,
- mocked_aws_server: ThreadedMotoServer,
- reset_aws_server_state: None,
- monkeypatch: pytest.MonkeyPatch,
-) -> EnvVarsDict:
- changed_envs = {
- "EC2_CLUSTERS_KEEPER_ENDPOINT": f"http://{mocked_aws_server._ip_address}:{mocked_aws_server._port}", # pylint: disable=protected-access # noqa: SLF001
- "EC2_CLUSTERS_KEEPER_ACCESS_KEY_ID": "xxx",
- "EC2_CLUSTERS_KEEPER_SECRET_ACCESS_KEY": "xxx",
- }
- return app_environment | setenvs_from_dict(monkeypatch, changed_envs)
-
-
@pytest.fixture
def aws_allowed_ec2_instance_type_names_env(
app_environment: EnvVarsDict,
@@ -241,147 +223,17 @@ def aws_allowed_ec2_instance_type_names_env(
return app_environment | setenvs_from_dict(monkeypatch, changed_envs)
-@pytest.fixture(scope="session")
-def vpc_cidr_block() -> str:
- return "10.0.0.0/16"
-
-
-@pytest.fixture
-async def aws_vpc_id(
- mocked_aws_server_envs: None,
- app_environment: EnvVarsDict,
- monkeypatch: pytest.MonkeyPatch,
- ec2_client: EC2Client,
- vpc_cidr_block: str,
-) -> AsyncIterator[str]:
- vpc = await ec2_client.create_vpc(
- CidrBlock=vpc_cidr_block,
- )
- vpc_id = vpc["Vpc"]["VpcId"] # type: ignore
- print(f"--> Created Vpc in AWS with {vpc_id=}")
- yield vpc_id
-
- await ec2_client.delete_vpc(VpcId=vpc_id)
- print(f"<-- Deleted Vpc in AWS with {vpc_id=}")
-
-
-@pytest.fixture(scope="session")
-def subnet_cidr_block() -> str:
- return "10.0.1.0/24"
-
-
-@pytest.fixture
-async def aws_subnet_id(
- monkeypatch: pytest.MonkeyPatch,
- aws_vpc_id: str,
- ec2_client: EC2Client,
- subnet_cidr_block: str,
-) -> AsyncIterator[str]:
- subnet = await ec2_client.create_subnet(
- CidrBlock=subnet_cidr_block, VpcId=aws_vpc_id
- )
- assert "Subnet" in subnet
- assert "SubnetId" in subnet["Subnet"]
- subnet_id = subnet["Subnet"]["SubnetId"]
- print(f"--> Created Subnet in AWS with {subnet_id=}")
-
- monkeypatch.setenv("PRIMARY_EC2_INSTANCES_SUBNET_ID", subnet_id)
- yield subnet_id
-
- # all the instances in the subnet must be terminated before that works
- instances_in_subnet = await ec2_client.describe_instances(
- Filters=[{"Name": "subnet-id", "Values": [subnet_id]}]
- )
- if instances_in_subnet["Reservations"]:
- print(f"--> terminating {len(instances_in_subnet)} instances in subnet")
- await ec2_client.terminate_instances(
- InstanceIds=[
- instance["Instances"][0]["InstanceId"] # type: ignore
- for instance in instances_in_subnet["Reservations"]
- ]
- )
- print(f"<-- terminated {len(instances_in_subnet)} instances in subnet")
-
- await ec2_client.delete_subnet(SubnetId=subnet_id)
- subnets = await ec2_client.describe_subnets()
- print(f"<-- Deleted Subnet in AWS with {subnet_id=}")
- print(f"current {subnets=}")
-
-
-@pytest.fixture
-async def aws_security_group_id(
- monkeypatch: pytest.MonkeyPatch,
- faker: Faker,
- aws_vpc_id: str,
- ec2_client: EC2Client,
-) -> AsyncIterator[str]:
- security_group = await ec2_client.create_security_group(
- Description=faker.text(), GroupName=faker.pystr(), VpcId=aws_vpc_id
- )
- security_group_id = security_group["GroupId"]
- print(f"--> Created Security Group in AWS with {security_group_id=}")
- monkeypatch.setenv(
- "PRIMARY_EC2_INSTANCES_SECURITY_GROUP_IDS",
- json.dumps([security_group_id]),
- )
- yield security_group_id
- await ec2_client.delete_security_group(GroupId=security_group_id)
- print(f"<-- Deleted Security Group in AWS with {security_group_id=}")
-
-
-@pytest.fixture
-async def aws_ami_id(
- app_environment: EnvVarsDict,
- mocked_aws_server_envs: None,
- monkeypatch: pytest.MonkeyPatch,
- ec2_client: EC2Client,
-) -> str:
- images = await ec2_client.describe_images()
- image = random.choice(images["Images"]) # noqa S311
- ami_id = image["ImageId"] # type: ignore
- monkeypatch.setenv("PRIMARY_EC2_INSTANCES_AMI_ID", ami_id)
- return ami_id
-
-
@pytest.fixture
async def clusters_keeper_ec2(
app_environment: EnvVarsDict,
-) -> AsyncIterator[ClustersKeeperEC2]:
- settings = EC2ClustersKeeperSettings.create_from_envs()
- ec2 = await ClustersKeeperEC2.create(settings)
+) -> AsyncIterator[SimcoreEC2API]:
+ settings = EC2Settings.create_from_envs()
+ ec2 = await SimcoreEC2API.create(settings)
assert ec2
yield ec2
await ec2.close()
-@pytest.fixture
-async def ec2_client(
- clusters_keeper_ec2: ClustersKeeperEC2,
-) -> EC2Client:
- return clusters_keeper_ec2.client
-
-
-@pytest.fixture
-def fake_ec2_instance_data(faker: Faker) -> Callable[..., EC2InstanceData]:
- def _creator(**overrides) -> EC2InstanceData:
- return EC2InstanceData(
- **(
- {
- "launch_time": faker.date_time(tzinfo=timezone.utc),
- "id": faker.uuid4(),
- "aws_private_dns": faker.name(),
- "aws_public_ip": faker.ipv4_public(),
- "type": faker.pystr(),
- "state": faker.pystr(),
- "tags": faker.pydict(allowed_types=(str,)),
- }
- | overrides
- )
- )
-
- return _creator
-
-
@pytest.fixture
async def mocked_redis_server(mocker: MockerFixture) -> None:
mock_redis = FakeRedis()
diff --git a/services/clusters-keeper/tests/unit/test_api_health.py b/services/clusters-keeper/tests/unit/test_api_health.py
index 411081de36d..dcd67cb4e98 100644
--- a/services/clusters-keeper/tests/unit/test_api_health.py
+++ b/services/clusters-keeper/tests/unit/test_api_health.py
@@ -20,7 +20,7 @@
def app_environment(
app_environment: EnvVarsDict,
enabled_rabbitmq: None,
- mocked_aws_server_envs: None,
+ mocked_ec2_server_envs: EnvVarsDict,
mocked_redis_server: None,
) -> EnvVarsDict:
return app_environment
diff --git a/services/clusters-keeper/tests/unit/test_modules_clusters.py b/services/clusters-keeper/tests/unit/test_modules_clusters.py
index 945bb781fde..6d0e82cd8da 100644
--- a/services/clusters-keeper/tests/unit/test_modules_clusters.py
+++ b/services/clusters-keeper/tests/unit/test_modules_clusters.py
@@ -5,22 +5,23 @@
import asyncio
import datetime
-from typing import Awaitable, Callable
+from collections.abc import Awaitable, Callable
import arrow
import pytest
+from aws_library.ec2.models import EC2InstanceData
from faker import Faker
from fastapi import FastAPI
from models_library.users import UserID
from models_library.wallets import WalletID
from parse import Result, search
+from pytest_simcore.helpers.utils_envs import EnvVarsDict
from simcore_service_clusters_keeper._meta import VERSION as APP_VERSION
from simcore_service_clusters_keeper.core.errors import Ec2InstanceNotFoundError
from simcore_service_clusters_keeper.core.settings import (
ApplicationSettings,
get_application_settings,
)
-from simcore_service_clusters_keeper.models import EC2InstanceData
from simcore_service_clusters_keeper.modules.clusters import (
cluster_heartbeat,
create_cluster,
@@ -50,11 +51,9 @@ def wallet_id(faker: Faker) -> WalletID:
def _base_configuration(
docker_swarm: None,
disabled_rabbitmq: None,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- aws_allowed_ec2_instance_type_names_env: list[str],
mocked_redis_server: None,
+ mocked_ec2_server_envs: EnvVarsDict,
+ mocked_primary_ec2_instances_envs: EnvVarsDict,
) -> None:
...
diff --git a/services/clusters-keeper/tests/unit/test_modules_clusters_management_core.py b/services/clusters-keeper/tests/unit/test_modules_clusters_management_core.py
index 4801c0233be..e09cc48c7dd 100644
--- a/services/clusters-keeper/tests/unit/test_modules_clusters_management_core.py
+++ b/services/clusters-keeper/tests/unit/test_modules_clusters_management_core.py
@@ -9,6 +9,7 @@
import pytest
from attr import dataclass
+from aws_library.ec2.models import EC2InstanceData
from faker import Faker
from fastapi import FastAPI
from models_library.users import UserID
@@ -16,7 +17,6 @@
from pytest_mock import MockerFixture
from pytest_simcore.helpers.typing_env import EnvVarsDict
from pytest_simcore.helpers.utils_envs import setenvs_from_dict
-from simcore_service_clusters_keeper.models import EC2InstanceData
from simcore_service_clusters_keeper.modules.clusters import (
cluster_heartbeat,
create_cluster,
@@ -58,12 +58,10 @@ def app_environment(
@pytest.fixture
def _base_configuration(
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- aws_allowed_ec2_instance_type_names_env: list[str],
disabled_rabbitmq: None,
mocked_redis_server: None,
+ mocked_ec2_server_envs: EnvVarsDict,
+ mocked_primary_ec2_instances_envs: EnvVarsDict,
) -> None:
...
diff --git a/services/clusters-keeper/tests/unit/test_modules_clusters_management_task.py b/services/clusters-keeper/tests/unit/test_modules_clusters_management_task.py
index 18d440aa541..d738a5cc05e 100644
--- a/services/clusters-keeper/tests/unit/test_modules_clusters_management_task.py
+++ b/services/clusters-keeper/tests/unit/test_modules_clusters_management_task.py
@@ -36,7 +36,7 @@ def mock_background_task(mocker: MockerFixture) -> mock.Mock:
async def test_clusters_management_task_created_and_deleted(
disabled_rabbitmq: None,
- mocked_aws_server_envs: None,
+ mocked_ec2_server_envs: EnvVarsDict,
mocked_redis_server: None,
mock_background_task: mock.Mock,
initialized_app: FastAPI,
diff --git a/services/clusters-keeper/tests/unit/test_modules_ec2.py b/services/clusters-keeper/tests/unit/test_modules_ec2.py
index eb253cbe289..0820ada5818 100644
--- a/services/clusters-keeper/tests/unit/test_modules_ec2.py
+++ b/services/clusters-keeper/tests/unit/test_modules_ec2.py
@@ -2,71 +2,11 @@
# pylint: disable=unused-argument
# pylint: disable=unused-variable
-from collections.abc import Callable
-from typing import cast
-import botocore.exceptions
import pytest
-from faker import Faker
from fastapi import FastAPI
-from moto.server import ThreadedMotoServer
-from pytest_mock.plugin import MockerFixture
-from pytest_simcore.helpers.utils_envs import EnvVarsDict
-from simcore_service_clusters_keeper.core.errors import (
- ConfigurationError,
- Ec2InstanceNotFoundError,
- Ec2InstanceTypeInvalidError,
- Ec2TooManyInstancesError,
-)
-from simcore_service_clusters_keeper.core.settings import (
- ApplicationSettings,
- EC2ClustersKeeperSettings,
-)
-from simcore_service_clusters_keeper.modules.ec2 import (
- ClustersKeeperEC2,
- EC2InstanceData,
- get_ec2_client,
-)
-from types_aiobotocore_ec2 import EC2Client
-from types_aiobotocore_ec2.literals import InstanceTypeType
-
-
-@pytest.fixture
-def ec2_settings(
- app_environment: EnvVarsDict,
-) -> EC2ClustersKeeperSettings:
- return EC2ClustersKeeperSettings.create_from_envs()
-
-
-@pytest.fixture
-def app_settings(
- app_environment: EnvVarsDict,
-) -> ApplicationSettings:
- return ApplicationSettings.create_from_envs()
-
-
-async def test_ec2_client_lifespan(ec2_settings: EC2ClustersKeeperSettings):
- ec2 = await ClustersKeeperEC2.create(settings=ec2_settings)
- assert ec2
- assert ec2.client
- assert ec2.exit_stack
- assert ec2.session
-
- await ec2.close()
-
-
-async def test_ec2_client_raises_when_no_connection_available(ec2_client: EC2Client):
- with pytest.raises(
- botocore.exceptions.ClientError, match=r".+ AWS was not able to validate .+"
- ):
- await ec2_client.describe_account_attributes(DryRun=True)
-
-
-async def test_ec2_client_with_mock_server(
- mocked_aws_server_envs: None, ec2_client: EC2Client
-):
- # passes without exception
- await ec2_client.describe_account_attributes(DryRun=True)
+from simcore_service_clusters_keeper.core.errors import ConfigurationError
+from simcore_service_clusters_keeper.modules.ec2 import get_ec2_client
async def test_ec2_does_not_initialize_if_deactivated(
@@ -79,279 +19,3 @@ async def test_ec2_does_not_initialize_if_deactivated(
assert initialized_app.state.ec2_client is None
with pytest.raises(ConfigurationError):
get_ec2_client(initialized_app)
-
-
-async def test_ec2_client_when_ec2_server_goes_up_and_down(
- mocked_aws_server: ThreadedMotoServer,
- mocked_aws_server_envs: None,
- ec2_client: EC2Client,
-):
- # passes without exception
- await ec2_client.describe_account_attributes(DryRun=True)
- mocked_aws_server.stop()
- with pytest.raises(botocore.exceptions.EndpointConnectionError):
- await ec2_client.describe_account_attributes(DryRun=True)
-
- # restart
- mocked_aws_server.start()
- # passes without exception
- await ec2_client.describe_account_attributes(DryRun=True)
-
-
-async def test_ping(
- mocked_aws_server: ThreadedMotoServer,
- mocked_aws_server_envs: None,
- aws_allowed_ec2_instance_type_names_env: list[str],
- app_settings: ApplicationSettings,
- clusters_keeper_ec2: ClustersKeeperEC2,
-):
- assert await clusters_keeper_ec2.ping() is True
- mocked_aws_server.stop()
- assert await clusters_keeper_ec2.ping() is False
- mocked_aws_server.start()
- assert await clusters_keeper_ec2.ping() is True
-
-
-async def test_get_ec2_instance_capabilities(
- mocked_aws_server_envs: None,
- aws_allowed_ec2_instance_type_names_env: list[str],
- app_settings: ApplicationSettings,
- clusters_keeper_ec2: ClustersKeeperEC2,
-):
- assert app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES
- assert (
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_ALLOWED_TYPES
- )
- instance_types = await clusters_keeper_ec2.get_ec2_instance_capabilities(
- cast(
- set[InstanceTypeType],
- set(
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_ALLOWED_TYPES
- ),
- )
- )
- assert instance_types
- assert len(instance_types) == len(
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_ALLOWED_TYPES
- )
-
- # all the instance names are found and valid
- assert all(
- i.name
- in app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_ALLOWED_TYPES
- for i in instance_types
- )
- for (
- instance_type_name
- ) in (
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_ALLOWED_TYPES
- ):
- assert any(i.name == instance_type_name for i in instance_types)
-
-
-async def test_get_ec2_instance_capabilities_with_empty_set_returns_all_options(
- mocked_aws_server_envs: None,
- clusters_keeper_ec2: ClustersKeeperEC2,
-):
- instance_types = await clusters_keeper_ec2.get_ec2_instance_capabilities(set())
- assert instance_types
- # NOTE: this might need adaptation when moto is updated
- assert 700 < len(instance_types) < 800
-
-
-async def test_get_ec2_instance_capabilities_with_invalid_names(
- mocked_aws_server_envs: None, clusters_keeper_ec2: ClustersKeeperEC2, faker: Faker
-):
- with pytest.raises(Ec2InstanceTypeInvalidError):
- await clusters_keeper_ec2.get_ec2_instance_capabilities(
- faker.pyset(allowed_types=(str,))
- )
-
-
-async def test_start_aws_instance(
- mocked_aws_server_envs: None,
- aws_vpc_id: str,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- ec2_client: EC2Client,
- clusters_keeper_ec2: ClustersKeeperEC2,
- app_settings: ApplicationSettings,
- faker: Faker,
- mocker: MockerFixture,
-):
- assert app_settings.CLUSTERS_KEEPER_EC2_ACCESS
- assert app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES
- # we have nothing running now in ec2
- all_instances = await ec2_client.describe_instances()
- assert not all_instances["Reservations"]
-
- instance_type = faker.pystr()
- tags = faker.pydict(allowed_types=(str,))
- startup_script = faker.pystr()
- await clusters_keeper_ec2.start_aws_instance(
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES,
- instance_type,
- tags=tags,
- startup_script=startup_script,
- number_of_instances=1,
- )
-
- # check we have that now in ec2
- all_instances = await ec2_client.describe_instances()
- assert len(all_instances["Reservations"]) == 1
- running_instance = all_instances["Reservations"][0]
- assert "Instances" in running_instance
- assert len(running_instance["Instances"]) == 1
- running_instance = running_instance["Instances"][0]
- assert "InstanceType" in running_instance
- assert running_instance["InstanceType"] == instance_type
- assert "Tags" in running_instance
- assert running_instance["Tags"] == [
- {"Key": key, "Value": value} for key, value in tags.items()
- ]
-
-
-async def test_start_aws_instance_is_limited_in_number_of_instances(
- mocked_aws_server_envs: None,
- aws_vpc_id: str,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- ec2_client: EC2Client,
- clusters_keeper_ec2: ClustersKeeperEC2,
- app_settings: ApplicationSettings,
- faker: Faker,
- mocker: MockerFixture,
-):
- assert app_settings.CLUSTERS_KEEPER_EC2_ACCESS
- assert app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES
- # we have nothing running now in ec2
- all_instances = await ec2_client.describe_instances()
- assert not all_instances["Reservations"]
-
- # create as many instances as we can
- tags = faker.pydict(allowed_types=(str,))
- startup_script = faker.pystr()
- for _ in range(
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_MAX_INSTANCES
- ):
- await clusters_keeper_ec2.start_aws_instance(
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES,
- faker.pystr(),
- tags=tags,
- startup_script=startup_script,
- number_of_instances=1,
- )
-
- # now creating one more shall fail
- with pytest.raises(Ec2TooManyInstancesError):
- await clusters_keeper_ec2.start_aws_instance(
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES,
- faker.pystr(),
- tags=tags,
- startup_script=startup_script,
- number_of_instances=1,
- )
-
-
-async def test_get_instances(
- mocked_aws_server_envs: None,
- aws_vpc_id: str,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- ec2_client: EC2Client,
- clusters_keeper_ec2: ClustersKeeperEC2,
- app_settings: ApplicationSettings,
- faker: Faker,
- mocker: MockerFixture,
-):
- assert app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES
- # we have nothing running now in ec2
- all_instances = await ec2_client.describe_instances()
- assert not all_instances["Reservations"]
- assert (
- await clusters_keeper_ec2.get_instances(
- key_names=[
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_KEY_NAME
- ],
- tags={},
- )
- == []
- )
-
- # create some instance
- instance_type = faker.pystr()
- tags = faker.pydict(allowed_types=(str,))
- startup_script = faker.pystr()
- created_instances = await clusters_keeper_ec2.start_aws_instance(
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES,
- instance_type,
- tags=tags,
- startup_script=startup_script,
- number_of_instances=1,
- )
- assert len(created_instances) == 1
-
- instance_received = await clusters_keeper_ec2.get_instances(
- key_names=[
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_KEY_NAME
- ],
- tags=tags,
- )
- assert created_instances == instance_received
-
-
-async def test_terminate_instance(
- mocked_aws_server_envs: None,
- aws_vpc_id: str,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- ec2_client: EC2Client,
- clusters_keeper_ec2: ClustersKeeperEC2,
- app_settings: ApplicationSettings,
- faker: Faker,
- mocker: MockerFixture,
-):
- assert app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES
- # we have nothing running now in ec2
- all_instances = await ec2_client.describe_instances()
- assert not all_instances["Reservations"]
- # create some instance
- instance_type = faker.pystr()
- tags = faker.pydict(allowed_types=(str,))
- startup_script = faker.pystr()
- created_instances = await clusters_keeper_ec2.start_aws_instance(
- app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES,
- instance_type,
- tags=tags,
- startup_script=startup_script,
- number_of_instances=1,
- )
- assert len(created_instances) == 1
-
- # terminate the instance
- await clusters_keeper_ec2.terminate_instances(created_instances)
- # calling it several times is ok, the instance stays a while
- await clusters_keeper_ec2.terminate_instances(created_instances)
-
-
-async def test_terminate_instance_not_existing_raises(
- mocked_aws_server_envs: None,
- aws_vpc_id: str,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- ec2_client: EC2Client,
- clusters_keeper_ec2: ClustersKeeperEC2,
- app_settings: ApplicationSettings,
- fake_ec2_instance_data: Callable[..., EC2InstanceData],
-):
- assert app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES
- # we have nothing running now in ec2
- all_instances = await ec2_client.describe_instances()
- assert not all_instances["Reservations"]
- with pytest.raises(Ec2InstanceNotFoundError):
- await clusters_keeper_ec2.terminate_instances([fake_ec2_instance_data()])
diff --git a/services/clusters-keeper/tests/unit/test_rpc_clusters.py b/services/clusters-keeper/tests/unit/test_rpc_clusters.py
index 34d4dad6fec..7d84e6425c8 100644
--- a/services/clusters-keeper/tests/unit/test_rpc_clusters.py
+++ b/services/clusters-keeper/tests/unit/test_rpc_clusters.py
@@ -15,6 +15,7 @@
from models_library.users import UserID
from models_library.wallets import WalletID
from pytest_mock.plugin import MockerFixture
+from pytest_simcore.helpers.typing_env import EnvVarsDict
from servicelib.rabbitmq import RabbitMQRPCClient
from servicelib.rabbitmq.rpc_interfaces.clusters_keeper.clusters import (
get_or_create_cluster,
@@ -43,11 +44,9 @@ def wallet_id(faker: Faker) -> WalletID:
def _base_configuration(
docker_swarm: None,
enabled_rabbitmq: None,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- aws_allowed_ec2_instance_type_names_env: list[str],
mocked_redis_server: None,
+ mocked_ec2_server_envs: EnvVarsDict,
+ mocked_primary_ec2_instances_envs: EnvVarsDict,
initialized_app: FastAPI,
) -> None:
...
diff --git a/services/clusters-keeper/tests/unit/test_rpc_ec2_instances.py b/services/clusters-keeper/tests/unit/test_rpc_ec2_instances.py
index ff8a506309f..d03b6b74502 100644
--- a/services/clusters-keeper/tests/unit/test_rpc_ec2_instances.py
+++ b/services/clusters-keeper/tests/unit/test_rpc_ec2_instances.py
@@ -4,7 +4,8 @@
import pytest
from fastapi import FastAPI
-from models_library.api_schemas_clusters_keeper.ec2_instances import EC2InstanceType
+from models_library.api_schemas_clusters_keeper.ec2_instances import EC2InstanceTypeGet
+from pytest_simcore.helpers.typing_env import EnvVarsDict
from servicelib.rabbitmq import RabbitMQRPCClient, RPCServerError
from servicelib.rabbitmq.rpc_interfaces.clusters_keeper.ec2_instances import (
get_instance_type_details,
@@ -21,11 +22,8 @@
def _base_configuration(
docker_swarm: None,
enabled_rabbitmq: None,
- aws_subnet_id: str,
- aws_security_group_id: str,
- aws_ami_id: str,
- aws_allowed_ec2_instance_type_names_env: list[str],
mocked_redis_server: None,
+ mocked_ec2_server_envs: EnvVarsDict,
initialized_app: FastAPI,
) -> None:
...
@@ -42,7 +40,7 @@ async def test_get_instance_type_details_all_options(
)
assert rpc_response
assert isinstance(rpc_response, list)
- assert isinstance(rpc_response[0], EC2InstanceType)
+ assert isinstance(rpc_response[0], EC2InstanceTypeGet)
async def test_get_instance_type_details_specific_type_names(
diff --git a/services/clusters-keeper/tests/unit/test_utils_clusters.py b/services/clusters-keeper/tests/unit/test_utils_clusters.py
index 495cdbb3d5f..3e772896da0 100644
--- a/services/clusters-keeper/tests/unit/test_utils_clusters.py
+++ b/services/clusters-keeper/tests/unit/test_utils_clusters.py
@@ -7,11 +7,11 @@
from typing import Any
import pytest
+from aws_library.ec2.models import EC2InstanceData
from faker import Faker
from models_library.api_schemas_clusters_keeper.clusters import ClusterState
from pytest_simcore.helpers.utils_envs import EnvVarsDict
from simcore_service_clusters_keeper.core.settings import ApplicationSettings
-from simcore_service_clusters_keeper.models import EC2InstanceData
from simcore_service_clusters_keeper.utils.clusters import (
create_cluster_from_ec2_instance,
create_startup_script,
@@ -26,7 +26,7 @@ def cluster_machines_name_prefix(faker: Faker) -> str:
def test_create_startup_script(
disabled_rabbitmq: None,
- mocked_aws_server_envs: EnvVarsDict,
+ mocked_ec2_server_envs: EnvVarsDict,
mocked_redis_server: None,
app_settings: ApplicationSettings,
cluster_machines_name_prefix: str,
diff --git a/services/clusters-keeper/tests/unit/test_utils_ec2.py b/services/clusters-keeper/tests/unit/test_utils_ec2.py
index e278d3dc890..51ccbe9a7da 100644
--- a/services/clusters-keeper/tests/unit/test_utils_ec2.py
+++ b/services/clusters-keeper/tests/unit/test_utils_ec2.py
@@ -6,6 +6,7 @@
from faker import Faker
from models_library.users import UserID
from models_library.wallets import WalletID
+from pytest_simcore.helpers.utils_envs import EnvVarsDict
from simcore_service_clusters_keeper.core.settings import ApplicationSettings
from simcore_service_clusters_keeper.utils.ec2 import (
_APPLICATION_TAG_KEY,
@@ -52,7 +53,7 @@ def test_get_cluster_name(
def test_creation_ec2_tags(
- mocked_aws_server_envs: None,
+ mocked_ec2_server_envs: EnvVarsDict,
disabled_rabbitmq: None,
mocked_redis_server: None,
app_settings: ApplicationSettings,
@@ -79,7 +80,7 @@ def test_creation_ec2_tags(
def test_all_created_ec2_instances_filter(
- mocked_aws_server_envs: None,
+ mocked_ec2_server_envs: EnvVarsDict,
disabled_rabbitmq: None,
mocked_redis_server: None,
app_settings: ApplicationSettings,
diff --git a/services/dask-sidecar/tests/unit/conftest.py b/services/dask-sidecar/tests/unit/conftest.py
index 97c916cf5b1..40fa1466bc3 100644
--- a/services/dask-sidecar/tests/unit/conftest.py
+++ b/services/dask-sidecar/tests/unit/conftest.py
@@ -25,7 +25,7 @@
from yarl import URL
pytest_plugins = [
- "pytest_simcore.aws_services",
+ "pytest_simcore.aws_server",
"pytest_simcore.cli_runner",
"pytest_simcore.docker_compose",
"pytest_simcore.docker_registry",
diff --git a/services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_tasks/_utils.py b/services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_tasks/_utils.py
index 57ede61ebd3..214b46d2c8c 100644
--- a/services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_tasks/_utils.py
+++ b/services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_tasks/_utils.py
@@ -5,7 +5,7 @@
import aiopg.sa
import arrow
from dask_task_models_library.container_tasks.protocol import ContainerEnvsDict
-from models_library.api_schemas_clusters_keeper.ec2_instances import EC2InstanceType
+from models_library.api_schemas_clusters_keeper.ec2_instances import EC2InstanceTypeGet
from models_library.api_schemas_directorv2.services import (
NodeRequirements,
ServiceExtras,
@@ -252,7 +252,7 @@ async def _update_project_node_resources_from_hardware_info(
return
try:
unordered_list_ec2_instance_types: list[
- EC2InstanceType
+ EC2InstanceTypeGet
] = await get_instance_type_details(
rabbitmq_rpc_client,
instance_type_names=set(hardware_info.aws_ec2_instances),
@@ -261,7 +261,7 @@ async def _update_project_node_resources_from_hardware_info(
assert unordered_list_ec2_instance_types # nosec
# NOTE: with the current implementation, there is no use to get the instance past the first one
- def _by_type_name(ec2: EC2InstanceType) -> bool:
+ def _by_type_name(ec2: EC2InstanceTypeGet) -> bool:
return bool(ec2.name == hardware_info.aws_ec2_instances[0])
selected_ec2_instance_type = next(
diff --git a/services/director-v2/tests/integration/02/test_dynamic_services_routes.py b/services/director-v2/tests/integration/02/test_dynamic_services_routes.py
index 8b52cbeae8f..32cbcbf5fc5 100644
--- a/services/director-v2/tests/integration/02/test_dynamic_services_routes.py
+++ b/services/director-v2/tests/integration/02/test_dynamic_services_routes.py
@@ -25,8 +25,8 @@
from models_library.users import UserID
from pytest_mock.plugin import MockerFixture
from pytest_simcore.helpers.typing_env import EnvVarsDict
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
from pytest_simcore.helpers.utils_envs import setenvs_from_dict
+from pytest_simcore.helpers.utils_host import get_localhost_ip
from servicelib.common_headers import (
X_DYNAMIC_SIDECAR_REQUEST_DNS,
X_DYNAMIC_SIDECAR_REQUEST_SCHEME,
diff --git a/services/director-v2/tests/integration/02/test_dynamic_sidecar_nodeports_integration.py b/services/director-v2/tests/integration/02/test_dynamic_sidecar_nodeports_integration.py
index 66f94472efa..ea71f7fbb44 100644
--- a/services/director-v2/tests/integration/02/test_dynamic_sidecar_nodeports_integration.py
+++ b/services/director-v2/tests/integration/02/test_dynamic_sidecar_nodeports_integration.py
@@ -46,8 +46,8 @@
from models_library.projects_state import RunningState
from models_library.users import UserID
from pydantic import AnyHttpUrl, parse_obj_as
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
from pytest_simcore.helpers.utils_envs import setenvs_from_dict
+from pytest_simcore.helpers.utils_host import get_localhost_ip
from servicelib.fastapi.long_running_tasks.client import (
Client,
ProgressMessage,
diff --git a/services/director-v2/tests/integration/02/test_mixed_dynamic_sidecar_and_legacy_project.py b/services/director-v2/tests/integration/02/test_mixed_dynamic_sidecar_and_legacy_project.py
index fae73a6e62e..ca528632c9d 100644
--- a/services/director-v2/tests/integration/02/test_mixed_dynamic_sidecar_and_legacy_project.py
+++ b/services/director-v2/tests/integration/02/test_mixed_dynamic_sidecar_and_legacy_project.py
@@ -20,8 +20,8 @@
from models_library.users import UserID
from pytest_mock.plugin import MockerFixture
from pytest_simcore.helpers.typing_env import EnvVarsDict
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
from pytest_simcore.helpers.utils_envs import setenvs_from_dict
+from pytest_simcore.helpers.utils_host import get_localhost_ip
from settings_library.rabbit import RabbitSettings
from settings_library.redis import RedisSettings
from utils import (
diff --git a/services/director-v2/tests/integration/02/utils.py b/services/director-v2/tests/integration/02/utils.py
index 209d98f3911..1dc81ec3c2f 100644
--- a/services/director-v2/tests/integration/02/utils.py
+++ b/services/director-v2/tests/integration/02/utils.py
@@ -21,7 +21,7 @@
)
from models_library.users import UserID
from pydantic import PositiveInt, parse_obj_as
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
+from pytest_simcore.helpers.utils_host import get_localhost_ip
from servicelib.common_headers import (
X_DYNAMIC_SIDECAR_REQUEST_DNS,
X_DYNAMIC_SIDECAR_REQUEST_SCHEME,
diff --git a/services/director-v2/tests/unit/with_dbs/test_api_route_computations.py b/services/director-v2/tests/unit/with_dbs/test_api_route_computations.py
index f3a83d487db..1cc63734366 100644
--- a/services/director-v2/tests/unit/with_dbs/test_api_route_computations.py
+++ b/services/director-v2/tests/unit/with_dbs/test_api_route_computations.py
@@ -21,7 +21,7 @@
import respx
from faker import Faker
from fastapi import FastAPI
-from models_library.api_schemas_clusters_keeper.ec2_instances import EC2InstanceType
+from models_library.api_schemas_clusters_keeper.ec2_instances import EC2InstanceTypeGet
from models_library.api_schemas_directorv2.comp_tasks import (
ComputationCreate,
ComputationGet,
@@ -407,7 +407,7 @@ def mocked_clusters_keeper_service_get_instance_type_details(
return mocker.patch(
"simcore_service_director_v2.modules.db.repositories.comp_tasks._utils.get_instance_type_details",
return_value=[
- EC2InstanceType(
+ EC2InstanceTypeGet(
name=default_pricing_plan_aws_ec2_type,
cpus=fake_ec2_cpus,
ram=fake_ec2_ram,
@@ -426,7 +426,7 @@ def mocked_clusters_keeper_service_get_instance_type_details_with_invalid_name(
return mocker.patch(
"simcore_service_director_v2.modules.db.repositories.comp_tasks._utils.get_instance_type_details",
return_value=[
- EC2InstanceType(
+ EC2InstanceTypeGet(
name=faker.pystr(),
cpus=fake_ec2_cpus,
ram=fake_ec2_ram,
diff --git a/services/dynamic-sidecar/tests/unit/test_modules_outputs_watcher.py b/services/dynamic-sidecar/tests/unit/test_modules_outputs_watcher.py
index 0d5e6f2ef17..d42108bbdc2 100644
--- a/services/dynamic-sidecar/tests/unit/test_modules_outputs_watcher.py
+++ b/services/dynamic-sidecar/tests/unit/test_modules_outputs_watcher.py
@@ -337,6 +337,7 @@ async def test_does_not_trigger_on_attribute_change(
assert mock_event_filter_upload_trigger.call_count == 1
+@pytest.mark.flaky(max_runs=3)
async def test_port_key_sequential_event_generation(
mock_long_running_upload_outputs: AsyncMock,
mounted_volumes: MountedVolumes,
diff --git a/services/osparc-gateway-server/tests/integration/conftest.py b/services/osparc-gateway-server/tests/integration/conftest.py
index 1e1161670ad..7ac15707662 100644
--- a/services/osparc-gateway-server/tests/integration/conftest.py
+++ b/services/osparc-gateway-server/tests/integration/conftest.py
@@ -4,7 +4,7 @@
import asyncio
import json
-from typing import Any, AsyncIterator, Awaitable, Callable, Union
+from typing import Any, AsyncIterator, Awaitable, Callable
import aiodocker
import dask_gateway
@@ -19,7 +19,7 @@
OSPARC_SCHEDULER_API_PORT,
OSPARC_SCHEDULER_DASHBOARD_PORT,
)
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
+from pytest_simcore.helpers.utils_host import get_localhost_ip
from tenacity._asyncio import AsyncRetrying
from tenacity.wait import wait_fixed
@@ -56,7 +56,7 @@ def gateway_password(faker: Faker) -> str:
return faker.password()
-def _convert_to_dict(c: Union[traitlets.config.Config, dict]) -> dict[str, Any]:
+def _convert_to_dict(c: traitlets.config.Config | dict) -> dict[str, Any]:
converted_dict = {}
for x, y in c.items():
if isinstance(y, (dict, traitlets.config.Config)):
diff --git a/services/osparc-gateway-server/tests/integration/test_clusters.py b/services/osparc-gateway-server/tests/integration/test_clusters.py
index 1ba132b9ef9..87f776bd5c3 100644
--- a/services/osparc-gateway-server/tests/integration/test_clusters.py
+++ b/services/osparc-gateway-server/tests/integration/test_clusters.py
@@ -11,7 +11,7 @@
from aiodocker import Docker
from dask_gateway import Gateway
from faker import Faker
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
+from pytest_simcore.helpers.utils_host import get_localhost_ip
from tenacity._asyncio import AsyncRetrying
from tenacity.stop import stop_after_delay
from tenacity.wait import wait_fixed
diff --git a/services/osparc-gateway-server/tests/integration/test_dask_sidecar.py b/services/osparc-gateway-server/tests/integration/test_dask_sidecar.py
index 4b46e5e226e..fa7e3554870 100644
--- a/services/osparc-gateway-server/tests/integration/test_dask_sidecar.py
+++ b/services/osparc-gateway-server/tests/integration/test_dask_sidecar.py
@@ -7,7 +7,7 @@
import aiodocker
import pytest
from faker import Faker
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
+from pytest_simcore.helpers.utils_host import get_localhost_ip
from tenacity._asyncio import AsyncRetrying
from tenacity.stop import stop_after_delay
from tenacity.wait import wait_fixed
diff --git a/services/osparc-gateway-server/tests/integration/test_gateway.py b/services/osparc-gateway-server/tests/integration/test_gateway.py
index 205772a0cee..44ae68ea3e3 100644
--- a/services/osparc-gateway-server/tests/integration/test_gateway.py
+++ b/services/osparc-gateway-server/tests/integration/test_gateway.py
@@ -9,7 +9,7 @@
from dask_gateway_server.app import DaskGateway
from faker import Faker
from osparc_gateway_server.backend.osparc import OsparcBackend
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
+from pytest_simcore.helpers.utils_host import get_localhost_ip
@pytest.fixture(
diff --git a/services/osparc-gateway-server/tests/system/test_deploy.py b/services/osparc-gateway-server/tests/system/test_deploy.py
index 4dd4e114ec3..79cc1221884 100644
--- a/services/osparc-gateway-server/tests/system/test_deploy.py
+++ b/services/osparc-gateway-server/tests/system/test_deploy.py
@@ -12,7 +12,7 @@
import dask_gateway
import pytest
from faker import Faker
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
+from pytest_simcore.helpers.utils_host import get_localhost_ip
from tenacity._asyncio import AsyncRetrying
from tenacity.stop import stop_after_delay
from tenacity.wait import wait_fixed
diff --git a/services/storage/tests/conftest.py b/services/storage/tests/conftest.py
index 531d4f67927..8864901f427 100644
--- a/services/storage/tests/conftest.py
+++ b/services/storage/tests/conftest.py
@@ -42,7 +42,7 @@
from moto.server import ThreadedMotoServer
from pydantic import ByteSize, parse_obj_as
from pytest_simcore.helpers.utils_assert import assert_status
-from pytest_simcore.helpers.utils_docker import get_localhost_ip
+from pytest_simcore.helpers.utils_host import get_localhost_ip
from simcore_postgres_database.storage_models import file_meta_data, projects, users
from simcore_service_storage.application import create
from simcore_service_storage.dsm import get_dsm_provider
From 125ae0b3f6c6af919af501ed78ed024c8defe365 Mon Sep 17 00:00:00 2001
From: Sylvain <35365065+sanderegg@users.noreply.github.com>
Date: Tue, 14 Nov 2023 15:54:46 +0100
Subject: [PATCH 07/24] =?UTF-8?q?=F0=9F=90=9BDynamic=20autoscaling:=20not?=
=?UTF-8?q?=20scaling=20beside=201=20machine=20(#5026)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/simcore_service_autoscaling/models.py | 15 +-
.../modules/auto_scaling_core.py | 162 ++++++++++++------
.../modules/auto_scaling_mode_base.py | 14 +-
.../auto_scaling_mode_computational.py | 15 +-
.../modules/auto_scaling_mode_dynamic.py | 18 +-
.../utils/auto_scaling_core.py | 79 +++++----
.../utils/computational_scaling.py | 41 +++--
.../utils/dynamic_scaling.py | 48 +++---
services/autoscaling/tests/manual/.env-devel | 5 +
services/autoscaling/tests/manual/Makefile | 4 +
services/autoscaling/tests/manual/README.md | 47 ++++-
services/autoscaling/tests/unit/conftest.py | 2 +-
...test_modules_auto_scaling_computational.py | 153 +++++++++--------
.../unit/test_modules_auto_scaling_dynamic.py | 34 ++--
.../unit/test_utils_computational_scaling.py | 32 ++--
.../tests/unit/test_utils_dynamic_scaling.py | 37 ++--
16 files changed, 438 insertions(+), 268 deletions(-)
diff --git a/services/autoscaling/src/simcore_service_autoscaling/models.py b/services/autoscaling/src/simcore_service_autoscaling/models.py
index 9e3ffec2df9..d9c3bf3978d 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/models.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/models.py
@@ -1,10 +1,23 @@
from dataclasses import dataclass, field
from typing import Any, TypeAlias
-from aws_library.ec2.models import EC2InstanceData
+from aws_library.ec2.models import EC2InstanceData, EC2InstanceType, Resources
from models_library.generated_models.docker_rest_api import Node
+@dataclass(frozen=True, kw_only=True)
+class AssignedTasksToInstance:
+ instance: EC2InstanceData
+ available_resources: Resources
+ assigned_tasks: list
+
+
+@dataclass(frozen=True, kw_only=True)
+class AssignedTasksToInstanceType:
+ instance_type: EC2InstanceType
+ assigned_tasks: list
+
+
@dataclass(frozen=True)
class AssociatedInstance:
node: Node
diff --git a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_core.py b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_core.py
index 382b7f85c6c..08cd815c5ad 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_core.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_core.py
@@ -30,7 +30,12 @@
Ec2TooManyInstancesError,
)
from ..core.settings import ApplicationSettings, get_application_settings
-from ..models import AssociatedInstance, Cluster
+from ..models import (
+ AssignedTasksToInstance,
+ AssignedTasksToInstanceType,
+ AssociatedInstance,
+ Cluster,
+)
from ..utils import utils_docker, utils_ec2
from ..utils.auto_scaling_core import (
associate_ec2_instances_with_nodes,
@@ -268,6 +273,56 @@ async def _activate_drained_nodes(
)
+async def _try_assign_tasks_to_instances(
+ app: FastAPI,
+ task,
+ auto_scaling_mode: BaseAutoscaling,
+ task_defined_ec2_type: InstanceTypeType | None,
+ active_instances_to_tasks: list[AssignedTasksToInstance],
+ pending_instances_to_tasks: list[AssignedTasksToInstance],
+ drained_instances_to_tasks: list[AssignedTasksToInstance],
+ needed_new_instance_types_for_tasks: list[AssignedTasksToInstanceType],
+) -> bool:
+ (
+ filtered_active_instance_to_task,
+ filtered_pending_instance_to_task,
+ filtered_drained_instances_to_task,
+ filtered_needed_new_instance_types_to_task,
+ ) = filter_by_task_defined_instance(
+ task_defined_ec2_type,
+ active_instances_to_tasks,
+ pending_instances_to_tasks,
+ drained_instances_to_tasks,
+ needed_new_instance_types_for_tasks,
+ )
+ # try to assign the task to one of the active, pending or net created instances
+ if (
+ await auto_scaling_mode.try_assigning_task_to_instances(
+ app,
+ task,
+ filtered_active_instance_to_task,
+ notify_progress=False,
+ )
+ or await auto_scaling_mode.try_assigning_task_to_instances(
+ app,
+ task,
+ filtered_pending_instance_to_task,
+ notify_progress=True,
+ )
+ or await auto_scaling_mode.try_assigning_task_to_instances(
+ app,
+ task,
+ filtered_drained_instances_to_task,
+ notify_progress=False,
+ )
+ or auto_scaling_mode.try_assigning_task_to_instance_types(
+ task, filtered_needed_new_instance_types_to_task
+ )
+ ):
+ return True
+ return False
+
+
async def _find_needed_instances(
app: FastAPI,
pending_tasks: list,
@@ -275,67 +330,51 @@ async def _find_needed_instances(
cluster: Cluster,
auto_scaling_mode: BaseAutoscaling,
) -> dict[EC2InstanceType, int]:
- type_to_instance_map = {t.name: t for t in available_ec2_types}
-
# 1. check first the pending task needs
- active_instances_to_tasks: list[tuple[EC2InstanceData, list]] = [
- (i.ec2_instance, []) for i in cluster.active_nodes
+ active_instances_to_tasks: list[AssignedTasksToInstance] = [
+ AssignedTasksToInstance(
+ instance=i.ec2_instance,
+ assigned_tasks=[],
+ available_resources=i.ec2_instance.resources
+ - await auto_scaling_mode.compute_node_used_resources(app, i),
+ )
+ for i in cluster.active_nodes
]
- pending_instances_to_tasks: list[tuple[EC2InstanceData, list]] = [
- (i, []) for i in cluster.pending_ec2s
+ pending_instances_to_tasks: list[AssignedTasksToInstance] = [
+ AssignedTasksToInstance(
+ instance=i, assigned_tasks=[], available_resources=i.resources
+ )
+ for i in cluster.pending_ec2s
]
- drained_instances_to_tasks: list[tuple[EC2InstanceData, list]] = [
- (i.ec2_instance, []) for i in cluster.drained_nodes
+ drained_instances_to_tasks: list[AssignedTasksToInstance] = [
+ AssignedTasksToInstance(
+ instance=i.ec2_instance,
+ assigned_tasks=[],
+ available_resources=i.ec2_instance.resources,
+ )
+ for i in cluster.drained_nodes
]
- needed_new_instance_types_for_tasks: list[tuple[EC2InstanceType, list]] = []
+ needed_new_instance_types_for_tasks: list[AssignedTasksToInstanceType] = []
for task in pending_tasks:
task_defined_ec2_type = await auto_scaling_mode.get_task_defined_instance(
app, task
)
- (
- filtered_active_instance_to_task,
- filtered_pending_instance_to_task,
- filtered_drained_instances_to_task,
- filtered_needed_new_instance_types_to_task,
- ) = filter_by_task_defined_instance(
+ _logger.info(
+ "task %s %s",
+ task,
+ f"defines ec2 type as {task_defined_ec2_type}"
+ if task_defined_ec2_type
+ else "does NOT define ec2 type",
+ )
+ if await _try_assign_tasks_to_instances(
+ app,
+ task,
+ auto_scaling_mode,
task_defined_ec2_type,
active_instances_to_tasks,
pending_instances_to_tasks,
drained_instances_to_tasks,
needed_new_instance_types_for_tasks,
- )
-
- # try to assign the task to one of the active, pending or net created instances
- _logger.debug(
- "Try to assign %s to any active/pending/created instance in the %s",
- f"{task}",
- f"{cluster=}",
- )
- if (
- await auto_scaling_mode.try_assigning_task_to_instances(
- app,
- task,
- filtered_active_instance_to_task,
- type_to_instance_map,
- notify_progress=False,
- )
- or await auto_scaling_mode.try_assigning_task_to_instances(
- app,
- task,
- filtered_pending_instance_to_task,
- type_to_instance_map,
- notify_progress=True,
- )
- or await auto_scaling_mode.try_assigning_task_to_instances(
- app,
- task,
- filtered_drained_instances_to_task,
- type_to_instance_map,
- notify_progress=False,
- )
- or auto_scaling_mode.try_assigning_task_to_instance_types(
- task, filtered_needed_new_instance_types_to_task
- )
):
continue
@@ -346,7 +385,11 @@ async def _find_needed_instances(
defined_ec2 = find_selected_instance_type_for_task(
task_defined_ec2_type, available_ec2_types, auto_scaling_mode, task
)
- needed_new_instance_types_for_tasks.append((defined_ec2, [task]))
+ needed_new_instance_types_for_tasks.append(
+ AssignedTasksToInstanceType(
+ instance_type=defined_ec2, assigned_tasks=[task]
+ )
+ )
else:
# we go for best fitting type
best_ec2_instance = utils_ec2.find_best_fitting_ec2_instance(
@@ -354,7 +397,11 @@ async def _find_needed_instances(
auto_scaling_mode.get_max_resources_from_task(task),
score_type=utils_ec2.closest_instance_policy,
)
- needed_new_instance_types_for_tasks.append((best_ec2_instance, [task]))
+ needed_new_instance_types_for_tasks.append(
+ AssignedTasksToInstanceType(
+ instance_type=best_ec2_instance, assigned_tasks=[task]
+ )
+ )
except Ec2InstanceNotFoundError:
_logger.exception(
"Task %s needs more resources than any EC2 instance "
@@ -365,7 +412,10 @@ async def _find_needed_instances(
_logger.exception("Unexpected error:")
num_instances_per_type = collections.defaultdict(
- int, collections.Counter(t for t, _ in needed_new_instance_types_for_tasks)
+ int,
+ collections.Counter(
+ t.instance_type for t in needed_new_instance_types_for_tasks
+ ),
)
# 2. check the buffer needs
@@ -379,9 +429,7 @@ async def _find_needed_instances(
) > 0:
# check if some are already pending
remaining_pending_instances = [
- instance
- for instance, assigned_tasks in pending_instances_to_tasks
- if not assigned_tasks
+ i.instance for i in pending_instances_to_tasks if not i.assigned_tasks
]
if len(remaining_pending_instances) < (
app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MACHINES_BUFFER
@@ -622,10 +670,14 @@ async def _autoscale_cluster(
) -> Cluster:
# 1. check if we have pending tasks and resolve them by activating some drained nodes
unrunnable_tasks = await auto_scaling_mode.list_unrunnable_tasks(app)
+ _logger.info("found %s unrunnable tasks", len(unrunnable_tasks))
# 2. try to activate drained nodes to cover some of the tasks
still_unrunnable_tasks, cluster = await _activate_drained_nodes(
app, cluster, unrunnable_tasks, auto_scaling_mode
)
+ _logger.info(
+ "still %s unrunnable tasks after node activation", len(still_unrunnable_tasks)
+ )
# let's check if there are still pending tasks or if the reserve was used
app_settings = get_application_settings(app)
diff --git a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_base.py b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_base.py
index 08ae67d435f..3c9e2742075 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_base.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_base.py
@@ -1,15 +1,18 @@
from abc import ABC, abstractmethod
-from collections.abc import Iterable
from dataclasses import dataclass
-from aws_library.ec2.models import EC2InstanceData, EC2InstanceType, Resources
+from aws_library.ec2.models import EC2InstanceData, Resources
from fastapi import FastAPI
from models_library.docker import DockerLabelKey
from models_library.generated_models.docker_rest_api import Node as DockerNode
from servicelib.logging_utils import LogLevelInt
from types_aiobotocore_ec2.literals import InstanceTypeType
-from ..models import AssociatedInstance
+from ..models import (
+ AssignedTasksToInstance,
+ AssignedTasksToInstanceType,
+ AssociatedInstance,
+)
@dataclass
@@ -48,8 +51,7 @@ def try_assigning_task_to_node(
async def try_assigning_task_to_instances(
app: FastAPI,
pending_task,
- instances_to_tasks: Iterable[tuple[EC2InstanceData, list]],
- type_to_instance_map: dict[str, EC2InstanceType],
+ instances_to_tasks: list[AssignedTasksToInstance],
*,
notify_progress: bool
) -> bool:
@@ -59,7 +61,7 @@ async def try_assigning_task_to_instances(
@abstractmethod
def try_assigning_task_to_instance_types(
pending_task,
- instance_types_to_tasks: Iterable[tuple[EC2InstanceType, list]],
+ instance_types_to_tasks: list[AssignedTasksToInstanceType],
) -> bool:
...
diff --git a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_computational.py b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_computational.py
index f627e0398a1..9ddb7df5c14 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_computational.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_computational.py
@@ -2,7 +2,7 @@
import logging
from collections.abc import Iterable
-from aws_library.ec2.models import EC2InstanceData, EC2InstanceType, Resources
+from aws_library.ec2.models import EC2InstanceData, Resources
from fastapi import FastAPI
from models_library.docker import (
DOCKER_TASK_EC2_INSTANCE_TYPE_PLACEMENT_CONSTRAINT_KEY,
@@ -15,7 +15,12 @@
from types_aiobotocore_ec2.literals import InstanceTypeType
from ..core.settings import get_application_settings
-from ..models import AssociatedInstance, DaskTask
+from ..models import (
+ AssignedTasksToInstance,
+ AssignedTasksToInstanceType,
+ AssociatedInstance,
+ DaskTask,
+)
from ..utils import computational_scaling as utils
from ..utils import utils_docker, utils_ec2
from . import dask
@@ -65,12 +70,10 @@ def try_assigning_task_to_node(
async def try_assigning_task_to_instances(
app: FastAPI,
pending_task,
- instances_to_tasks: Iterable[tuple[EC2InstanceData, list]],
- type_to_instance_map: dict[str, EC2InstanceType],
+ instances_to_tasks: list[AssignedTasksToInstance],
*,
notify_progress: bool
) -> bool:
- assert type_to_instance_map # nosec
return await utils.try_assigning_task_to_instances(
app,
pending_task,
@@ -81,7 +84,7 @@ async def try_assigning_task_to_instances(
@staticmethod
def try_assigning_task_to_instance_types(
pending_task,
- instance_types_to_tasks: Iterable[tuple[EC2InstanceType, list]],
+ instance_types_to_tasks: list[AssignedTasksToInstanceType],
) -> bool:
return utils.try_assigning_task_to_instance_types(
pending_task, instance_types_to_tasks
diff --git a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_dynamic.py b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_dynamic.py
index b2107f2a043..ec3870eb422 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_dynamic.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/modules/auto_scaling_mode_dynamic.py
@@ -1,6 +1,6 @@
from collections.abc import Iterable
-from aws_library.ec2.models import EC2InstanceData, EC2InstanceType, Resources
+from aws_library.ec2.models import EC2InstanceData, Resources
from fastapi import FastAPI
from models_library.docker import DockerLabelKey
from models_library.generated_models.docker_rest_api import Node, Task
@@ -8,7 +8,11 @@
from types_aiobotocore_ec2.literals import InstanceTypeType
from ..core.settings import get_application_settings
-from ..models import AssociatedInstance
+from ..models import (
+ AssignedTasksToInstance,
+ AssignedTasksToInstanceType,
+ AssociatedInstance,
+)
from ..utils import dynamic_scaling as utils
from ..utils import utils_docker, utils_ec2
from ..utils.rabbitmq import log_tasks_message, progress_tasks_message
@@ -57,25 +61,23 @@ def try_assigning_task_to_node(
async def try_assigning_task_to_instances(
app: FastAPI,
pending_task,
- instances_to_tasks: Iterable[tuple[EC2InstanceData, list]],
- type_to_instance_map: dict[str, EC2InstanceType],
+ instances_to_tasks: list[AssignedTasksToInstance],
*,
notify_progress: bool
) -> bool:
- return await utils.try_assigning_task_to_pending_instances(
+ return await utils.try_assigning_task_to_instances(
app,
pending_task,
instances_to_tasks,
- type_to_instance_map,
notify_progress=notify_progress,
)
@staticmethod
def try_assigning_task_to_instance_types(
pending_task,
- instance_types_to_tasks: Iterable[tuple[EC2InstanceType, list]],
+ instance_types_to_tasks: list[AssignedTasksToInstanceType],
) -> bool:
- return utils.try_assigning_task_to_instances(
+ return utils.try_assigning_task_to_instance_types(
pending_task, instance_types_to_tasks
)
diff --git a/services/autoscaling/src/simcore_service_autoscaling/utils/auto_scaling_core.py b/services/autoscaling/src/simcore_service_autoscaling/utils/auto_scaling_core.py
index 701f9d1ae39..106b68ea352 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/utils/auto_scaling_core.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/utils/auto_scaling_core.py
@@ -9,7 +9,11 @@
from ..core.errors import Ec2InstanceInvalidError, Ec2InvalidDnsNameError
from ..core.settings import ApplicationSettings
-from ..models import AssociatedInstance
+from ..models import (
+ AssignedTasksToInstance,
+ AssignedTasksToInstanceType,
+ AssociatedInstance,
+)
from ..modules.auto_scaling_mode_base import BaseAutoscaling
from . import utils_docker
@@ -108,52 +112,63 @@ def _instance_type_by_type_name(
def _instance_type_map_by_type_name(
- mapping: tuple[EC2InstanceType, list], *, type_name: InstanceTypeType | None
+ mapping: AssignedTasksToInstanceType, *, type_name: InstanceTypeType | None
) -> bool:
- ec2_type, _ = mapping
- return _instance_type_by_type_name(ec2_type, type_name=type_name)
+ return _instance_type_by_type_name(mapping.instance_type, type_name=type_name)
def _instance_data_map_by_type_name(
- mapping: tuple[EC2InstanceData, list], *, type_name: InstanceTypeType | None
+ mapping: AssignedTasksToInstance, *, type_name: InstanceTypeType | None
) -> bool:
if type_name is None:
return True
- ec2_data, _ = mapping
- return bool(ec2_data.type == type_name)
+ return bool(mapping.instance.type == type_name)
def filter_by_task_defined_instance(
instance_type_name: InstanceTypeType | None,
- active_instances_to_tasks,
- pending_instances_to_tasks,
- drained_instances_to_tasks,
- needed_new_instance_types_for_tasks,
-) -> tuple:
+ active_instances_to_tasks: list[AssignedTasksToInstance],
+ pending_instances_to_tasks: list[AssignedTasksToInstance],
+ drained_instances_to_tasks: list[AssignedTasksToInstance],
+ needed_new_instance_types_for_tasks: list[AssignedTasksToInstanceType],
+) -> tuple[
+ list[AssignedTasksToInstance],
+ list[AssignedTasksToInstance],
+ list[AssignedTasksToInstance],
+ list[AssignedTasksToInstanceType],
+]:
return (
- filter(
- functools.partial(
- _instance_data_map_by_type_name, type_name=instance_type_name
- ),
- active_instances_to_tasks,
+ list(
+ filter(
+ functools.partial(
+ _instance_data_map_by_type_name, type_name=instance_type_name
+ ),
+ active_instances_to_tasks,
+ )
),
- filter(
- functools.partial(
- _instance_data_map_by_type_name, type_name=instance_type_name
- ),
- pending_instances_to_tasks,
+ list(
+ filter(
+ functools.partial(
+ _instance_data_map_by_type_name, type_name=instance_type_name
+ ),
+ pending_instances_to_tasks,
+ )
),
- filter(
- functools.partial(
- _instance_data_map_by_type_name, type_name=instance_type_name
- ),
- drained_instances_to_tasks,
+ list(
+ filter(
+ functools.partial(
+ _instance_data_map_by_type_name, type_name=instance_type_name
+ ),
+ drained_instances_to_tasks,
+ )
),
- filter(
- functools.partial(
- _instance_type_map_by_type_name, type_name=instance_type_name
- ),
- needed_new_instance_types_for_tasks,
+ list(
+ filter(
+ functools.partial(
+ _instance_type_map_by_type_name, type_name=instance_type_name
+ ),
+ needed_new_instance_types_for_tasks,
+ )
),
)
diff --git a/services/autoscaling/src/simcore_service_autoscaling/utils/computational_scaling.py b/services/autoscaling/src/simcore_service_autoscaling/utils/computational_scaling.py
index 8fcefb5c8de..f7ff8253716 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/utils/computational_scaling.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/utils/computational_scaling.py
@@ -3,14 +3,19 @@
from collections.abc import Iterable
from typing import Final
-from aws_library.ec2.models import EC2InstanceData, EC2InstanceType, Resources
+from aws_library.ec2.models import Resources
from dask_task_models_library.constants import DASK_TASK_EC2_RESOURCE_RESTRICTION_KEY
from fastapi import FastAPI
from servicelib.utils_formatting import timedelta_as_minute_second
from types_aiobotocore_ec2.literals import InstanceTypeType
from ..core.settings import get_application_settings
-from ..models import AssociatedInstance, DaskTask
+from ..models import (
+ AssignedTasksToInstance,
+ AssignedTasksToInstanceType,
+ AssociatedInstance,
+ DaskTask,
+)
_logger = logging.getLogger(__name__)
@@ -54,7 +59,7 @@ def try_assigning_task_to_node(
async def try_assigning_task_to_instances(
app: FastAPI,
pending_task: DaskTask,
- instances_to_tasks: Iterable[tuple[EC2InstanceData, list[DaskTask]]],
+ instances_to_tasks: list[AssignedTasksToInstance],
*,
notify_progress: bool,
) -> bool:
@@ -63,20 +68,23 @@ async def try_assigning_task_to_instances(
instance_max_time_to_start = (
app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MAX_START_TIME
)
- for instance, instance_assigned_tasks in instances_to_tasks:
- instance_total_resources = instance.resources
+ for assigned_tasks_to_instance in instances_to_tasks:
tasks_needed_resources = _compute_tasks_needed_resources(
- instance_assigned_tasks
+ assigned_tasks_to_instance.assigned_tasks
)
if (
- instance_total_resources - tasks_needed_resources
+ assigned_tasks_to_instance.available_resources - tasks_needed_resources
) >= get_max_resources_from_dask_task(pending_task):
- instance_assigned_tasks.append(pending_task)
+ assigned_tasks_to_instance.assigned_tasks.append(pending_task)
if notify_progress:
now = datetime.datetime.now(datetime.timezone.utc)
- time_since_launch = now - instance.launch_time
+ time_since_launch = (
+ now - assigned_tasks_to_instance.instance.launch_time
+ )
estimated_time_to_completion = (
- instance.launch_time + instance_max_time_to_start - now
+ assigned_tasks_to_instance.instance.launch_time
+ + instance_max_time_to_start
+ - now
)
_logger.info(
"LOG: %s",
@@ -94,16 +102,19 @@ async def try_assigning_task_to_instances(
def try_assigning_task_to_instance_types(
pending_task: DaskTask,
- instance_types_to_tasks: Iterable[tuple[EC2InstanceType, list[DaskTask]]],
+ instance_types_to_tasks: list[AssignedTasksToInstanceType],
) -> bool:
- for instance, instance_assigned_tasks in instance_types_to_tasks:
- instance_total_resource = Resources(cpus=instance.cpus, ram=instance.ram)
+ for assigned_tasks_to_instance_type in instance_types_to_tasks:
+ instance_total_resource = Resources(
+ cpus=assigned_tasks_to_instance_type.instance_type.cpus,
+ ram=assigned_tasks_to_instance_type.instance_type.ram,
+ )
tasks_needed_resources = _compute_tasks_needed_resources(
- instance_assigned_tasks
+ assigned_tasks_to_instance_type.assigned_tasks
)
if (
instance_total_resource - tasks_needed_resources
) >= get_max_resources_from_dask_task(pending_task):
- instance_assigned_tasks.append(pending_task)
+ assigned_tasks_to_instance_type.assigned_tasks.append(pending_task)
return True
return False
diff --git a/services/autoscaling/src/simcore_service_autoscaling/utils/dynamic_scaling.py b/services/autoscaling/src/simcore_service_autoscaling/utils/dynamic_scaling.py
index 93af22f7f4e..ab6f6756d43 100644
--- a/services/autoscaling/src/simcore_service_autoscaling/utils/dynamic_scaling.py
+++ b/services/autoscaling/src/simcore_service_autoscaling/utils/dynamic_scaling.py
@@ -2,13 +2,17 @@
import logging
from collections.abc import Iterable
-from aws_library.ec2.models import EC2InstanceData, EC2InstanceType, Resources
+from aws_library.ec2.models import Resources
from fastapi import FastAPI
from models_library.generated_models.docker_rest_api import Task
from servicelib.utils_formatting import timedelta_as_minute_second
from ..core.settings import get_application_settings
-from ..models import AssociatedInstance
+from ..models import (
+ AssignedTasksToInstance,
+ AssignedTasksToInstanceType,
+ AssociatedInstance,
+)
from . import utils_docker
from .rabbitmq import log_tasks_message, progress_tasks_message
@@ -32,28 +36,30 @@ def try_assigning_task_to_node(
return False
-def try_assigning_task_to_instances(
+def try_assigning_task_to_instance_types(
pending_task: Task,
- instance_types_to_tasks: Iterable[tuple[EC2InstanceType, list[Task]]],
+ instance_types_to_tasks: list[AssignedTasksToInstanceType],
) -> bool:
- for instance, instance_assigned_tasks in instance_types_to_tasks:
- instance_total_resource = Resources(cpus=instance.cpus, ram=instance.ram)
+ for assigned_tasks_to_instance_type in instance_types_to_tasks:
+ instance_total_resource = Resources(
+ cpus=assigned_tasks_to_instance_type.instance_type.cpus,
+ ram=assigned_tasks_to_instance_type.instance_type.ram,
+ )
tasks_needed_resources = utils_docker.compute_tasks_needed_resources(
- instance_assigned_tasks
+ assigned_tasks_to_instance_type.assigned_tasks
)
if (
instance_total_resource - tasks_needed_resources
) >= utils_docker.get_max_resources_from_docker_task(pending_task):
- instance_assigned_tasks.append(pending_task)
+ assigned_tasks_to_instance_type.assigned_tasks.append(pending_task)
return True
return False
-async def try_assigning_task_to_pending_instances(
+async def try_assigning_task_to_instances(
app: FastAPI,
pending_task: Task,
- instances_to_tasks: Iterable[tuple[EC2InstanceData, list[Task]]],
- type_to_instance_map: dict[str, EC2InstanceType],
+ instances_to_tasks: list[AssignedTasksToInstance],
*,
notify_progress: bool,
) -> bool:
@@ -62,23 +68,23 @@ async def try_assigning_task_to_pending_instances(
instance_max_time_to_start = (
app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MAX_START_TIME
)
- for instance, instance_assigned_tasks in instances_to_tasks:
- instance_type = type_to_instance_map[instance.type]
- instance_total_resources = Resources(
- cpus=instance_type.cpus, ram=instance_type.ram
- )
+ for assigned_tasks_to_instance in instances_to_tasks:
tasks_needed_resources = utils_docker.compute_tasks_needed_resources(
- instance_assigned_tasks
+ assigned_tasks_to_instance.assigned_tasks
)
if (
- instance_total_resources - tasks_needed_resources
+ assigned_tasks_to_instance.available_resources - tasks_needed_resources
) >= utils_docker.get_max_resources_from_docker_task(pending_task):
- instance_assigned_tasks.append(pending_task)
+ assigned_tasks_to_instance.assigned_tasks.append(pending_task)
if notify_progress:
now = datetime.datetime.now(datetime.timezone.utc)
- time_since_launch = now - instance.launch_time
+ time_since_launch = (
+ now - assigned_tasks_to_instance.instance.launch_time
+ )
estimated_time_to_completion = (
- instance.launch_time + instance_max_time_to_start - now
+ assigned_tasks_to_instance.instance.launch_time
+ + instance_max_time_to_start
+ - now
)
await log_tasks_message(
diff --git a/services/autoscaling/tests/manual/.env-devel b/services/autoscaling/tests/manual/.env-devel
index 63bbdbb9fb2..80e54412ce4 100644
--- a/services/autoscaling/tests/manual/.env-devel
+++ b/services/autoscaling/tests/manual/.env-devel
@@ -10,6 +10,11 @@ EC2_INSTANCES_SUBNET_ID=XXXXXXXXXX
EC2_SECRET_ACCESS_KEY=XXXXXXXXXX
EC2_INSTANCES_NAME_PREFIX=testing-osparc-computational-cluster
LOG_FORMAT_LOCAL_DEV_ENABLED=True
+# define the following to activate dynamic autoscaling
+# NODES_MONITORING_NEW_NODES_LABELS="[\"testing.autoscaled-node\"]"
+# NODES_MONITORING_NODE_LABELS="[\"testing.monitored-node\"]"
+# NODES_MONITORING_SERVICE_LABELS="[\"testing.monitored-service\"]"
+
# may be activated or not
# RABBIT_HOST=rabbit
# RABBIT_PASSWORD=test
diff --git a/services/autoscaling/tests/manual/Makefile b/services/autoscaling/tests/manual/Makefile
index 48ab85d9068..a5f6be51ad4 100644
--- a/services/autoscaling/tests/manual/Makefile
+++ b/services/autoscaling/tests/manual/Makefile
@@ -18,10 +18,14 @@ include ../../../../scripts/common-service.Makefile
up-devel: .init-swarm .stack-devel.yml ## starts local test application
@docker stack deploy --with-registry-auth --compose-file=.stack-devel.yml autoscaling
+ # to follow logs of autoscaling, run
+ # docker service logs --follow autoscaling_autoscaling
up-computational-devel: .init-swarm .stack-computational-devel.yml ## starts local test application in computational mode
# DASK_MONITORING_URL set to $(DASK_MONITORING_URL)
@docker stack deploy --with-registry-auth --compose-file=.stack-computational-devel.yml comp-autoscaling
+ # to follow logs of autoscaling, run
+ # docker service logs --follow comp-autoscaling_autoscaling
down: .env ## stops local test app dependencies (running bare metal against AWS)
# remove stacks
diff --git a/services/autoscaling/tests/manual/README.md b/services/autoscaling/tests/manual/README.md
index a8be43853f5..e44e739d182 100644
--- a/services/autoscaling/tests/manual/README.md
+++ b/services/autoscaling/tests/manual/README.md
@@ -5,15 +5,17 @@ The autoscaling service may be started either in computational mode or in dynami
The computational mode is used in conjunction with a dask-scheduler/dask-worker subsystem.
The dynamic mode is used directly with docker swarm facilities.
-## computational mode
-
-When ```DASK_MONITORING_URL``` is set the computational mode is enabled.
-
### requirements
1. AWS EC2 access
2. a machine running in EC2 with docker installed and access to osparc-simcore repository
+
+## computational mode
+
+When ```DASK_MONITORING_URL``` is set the computational mode is enabled.
+
+
### instructions
1. prepare autoscaling
@@ -67,3 +69,40 @@ future.done() # shall return True once done
# remove the future from the dask-scheduler memory, shall trigger the autoscaling service to remove the created machine
del future
```
+
+
+## dynamic mode
+
+When ```NODES_MONITORING_NEW_NODES_LABELS```, ```NODES_MONITORING_NODE_LABELS``` and ```NODES_MONITORING_SERVICE_LABELS``` are set the dynamic mode is enabled.
+
+### instructions
+
+1. prepare autoscaling
+
+```bash
+# run on EC2 instance
+git clone https://github.com/ITISFoundation/osparc-simcore.git
+cd osparc-simcore/services/autoscaling
+make build-devel # this will build the autoscaling devel image
+```
+
+2. setup environment variables
+```bash
+# run on EC2 instance
+cd osparc-simcore/services/autoscaling/tests/manual
+make .env # generate an initial .env file
+nano .env # edit .env and set the variables as needed
+```
+
+3. start autoscaling stack
+```bash
+# run on EC2 instance
+cd osparc-simcore/services/autoscaling/tests/manual
+make up-devel # this will deploy the autoscaling stack
+```
+
+4. start some docker services to trigger autoscaling
+```bash
+# run on EC2 instance
+docker service create --name=test-service --reserve-cpu=4 --reserve-memory=1GiB --constraint=node.labels.testing.monitored-node==true --label=testing.monitored-service=true redis # will create a redis service reserving 4 CPUs and 1GiB of RAM
+```
diff --git a/services/autoscaling/tests/unit/conftest.py b/services/autoscaling/tests/unit/conftest.py
index fb6dca9f814..14f1132c531 100644
--- a/services/autoscaling/tests/unit/conftest.py
+++ b/services/autoscaling/tests/unit/conftest.py
@@ -597,7 +597,7 @@ def _creator(required_resources: DaskTaskResources) -> distributed.Future:
@pytest.fixture
-def mock_set_node_availability(mocker: MockerFixture) -> mock.Mock:
+def mock_docker_set_node_availability(mocker: MockerFixture) -> mock.Mock:
async def _fake_set_node_availability(
docker_client: AutoscalingDocker, node: Node, *, available: bool
) -> Node:
diff --git a/services/autoscaling/tests/unit/test_modules_auto_scaling_computational.py b/services/autoscaling/tests/unit/test_modules_auto_scaling_computational.py
index 7d4f1755ff3..9446bdfb2d2 100644
--- a/services/autoscaling/tests/unit/test_modules_auto_scaling_computational.py
+++ b/services/autoscaling/tests/unit/test_modules_auto_scaling_computational.py
@@ -163,7 +163,7 @@ def _assert_rabbit_autoscaling_message_sent(
@pytest.fixture
-def mock_tag_node(mocker: MockerFixture) -> mock.Mock:
+def mock_docker_tag_node(mocker: MockerFixture) -> mock.Mock:
async def fake_tag_node(*args, **kwargs) -> DockerNode:
return args[1]
@@ -175,7 +175,7 @@ async def fake_tag_node(*args, **kwargs) -> DockerNode:
@pytest.fixture
-def mock_find_node_with_name(
+def mock_docker_find_node_with_name(
mocker: MockerFixture, fake_node: DockerNode
) -> Iterator[mock.Mock]:
return mocker.patch(
@@ -186,7 +186,7 @@ def mock_find_node_with_name(
@pytest.fixture
-def mock_compute_node_used_resources(mocker: MockerFixture) -> mock.Mock:
+def mock_docker_compute_node_used_resources(mocker: MockerFixture) -> mock.Mock:
return mocker.patch(
"simcore_service_autoscaling.modules.auto_scaling_core.utils_docker.compute_node_used_resources",
autospec=True,
@@ -288,6 +288,24 @@ def _do(
return _do
+@pytest.fixture
+def mock_dask_get_worker_has_results_in_memory(mocker: MockerFixture) -> mock.Mock:
+ return mocker.patch(
+ "simcore_service_autoscaling.modules.dask.get_worker_still_has_results_in_memory",
+ return_value=0,
+ autospec=True,
+ )
+
+
+@pytest.fixture
+def mock_dask_get_worker_used_resources(mocker: MockerFixture) -> mock.Mock:
+ return mocker.patch(
+ "simcore_service_autoscaling.modules.dask.get_worker_used_resources",
+ return_value=Resources.create_as_empty(),
+ autospec=True,
+ )
+
+
@pytest.mark.acceptance_test()
@pytest.mark.parametrize(
"dask_task_imposed_ec2_type, dask_ram, expected_ec2_type",
@@ -324,12 +342,14 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
initialized_app: FastAPI,
create_dask_task: Callable[[DaskTaskResources], distributed.Future],
ec2_client: EC2Client,
- mock_tag_node: mock.Mock,
+ mock_docker_tag_node: mock.Mock,
fake_node: DockerNode,
mock_rabbitmq_post_message: mock.Mock,
- mock_find_node_with_name: mock.Mock,
- mock_set_node_availability: mock.Mock,
- mock_compute_node_used_resources: mock.Mock,
+ mock_docker_find_node_with_name: mock.Mock,
+ mock_docker_set_node_availability: mock.Mock,
+ mock_docker_compute_node_used_resources: mock.Mock,
+ mock_dask_get_worker_has_results_in_memory: mock.Mock,
+ mock_dask_get_worker_used_resources: mock.Mock,
mocker: MockerFixture,
dask_spec_local_cluster: distributed.SpecCluster,
create_dask_task_resources: Callable[..., DaskTaskResources],
@@ -376,10 +396,12 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
)
# as the new node is already running, but is not yet connected, hence not tagged and drained
- mock_find_node_with_name.assert_not_called()
- mock_tag_node.assert_not_called()
- mock_set_node_availability.assert_not_called()
- mock_compute_node_used_resources.assert_not_called()
+ mock_docker_find_node_with_name.assert_not_called()
+ mock_docker_tag_node.assert_not_called()
+ mock_docker_set_node_availability.assert_not_called()
+ mock_docker_compute_node_used_resources.assert_not_called()
+ mock_dask_get_worker_has_results_in_memory.assert_not_called()
+ mock_dask_get_worker_used_resources.assert_not_called()
# check rabbit messages were sent
_assert_rabbit_autoscaling_message_sent(
mock_rabbitmq_post_message,
@@ -395,7 +417,10 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
await auto_scale_cluster(
app=initialized_app, auto_scaling_mode=ComputationalAutoscaling()
)
- mock_compute_node_used_resources.assert_not_called()
+ mock_dask_get_worker_has_results_in_memory.assert_called_once()
+ mock_dask_get_worker_has_results_in_memory.reset_mock()
+ mock_dask_get_worker_used_resources.assert_called_once()
+ mock_dask_get_worker_used_resources.reset_mock()
internal_dns_names = await _assert_ec2_instances(
ec2_client,
num_reservations=1,
@@ -407,25 +432,24 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
internal_dns_name = internal_dns_names[0].removesuffix(".ec2.internal")
# the node is tagged and made active right away since we still have the pending task
- mock_find_node_with_name.assert_called_once()
- mock_find_node_with_name.reset_mock()
+ mock_docker_find_node_with_name.assert_called_once()
+ mock_docker_find_node_with_name.reset_mock()
expected_docker_node_tags = {
DOCKER_TASK_EC2_INSTANCE_TYPE_PLACEMENT_CONSTRAINT_KEY: expected_ec2_type
}
- mock_tag_node.assert_called_once_with(
+ mock_docker_tag_node.assert_called_once_with(
get_docker_client(initialized_app),
fake_node,
tags=expected_docker_node_tags,
available=False,
)
- mock_tag_node.reset_mock()
- mock_set_node_availability.assert_called_once_with(
+ mock_docker_tag_node.reset_mock()
+ mock_docker_set_node_availability.assert_called_once_with(
get_docker_client(initialized_app), fake_node, available=True
)
- mock_set_node_availability.reset_mock()
-
- # in this case there is no message sent since the worker was not started yet
- mock_rabbitmq_post_message.assert_not_called()
+ mock_docker_set_node_availability.reset_mock()
+ mock_rabbitmq_post_message.assert_called_once()
+ mock_rabbitmq_post_message.reset_mock()
# now we have 1 monitored node that needs to be mocked
auto_scaling_mode = ComputationalAutoscaling()
@@ -442,14 +466,22 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
fake_node.Description.Hostname = internal_dns_name
# 3. calling this multiple times should do nothing
- for _ in range(10):
+ num_useless_calls = 10
+ for _ in range(num_useless_calls):
await auto_scale_cluster(
app=initialized_app, auto_scaling_mode=auto_scaling_mode
)
- mock_compute_node_used_resources.assert_not_called()
- mock_find_node_with_name.assert_not_called()
- mock_tag_node.assert_not_called()
- mock_set_node_availability.assert_not_called()
+ mock_dask_get_worker_has_results_in_memory.assert_called()
+ assert (
+ mock_dask_get_worker_has_results_in_memory.call_count == 2 * num_useless_calls
+ )
+ mock_dask_get_worker_has_results_in_memory.reset_mock()
+ mock_dask_get_worker_used_resources.assert_called()
+ assert mock_dask_get_worker_used_resources.call_count == 2 * num_useless_calls
+ mock_dask_get_worker_used_resources.reset_mock()
+ mock_docker_find_node_with_name.assert_not_called()
+ mock_docker_tag_node.assert_not_called()
+ mock_docker_set_node_availability.assert_not_called()
# check the number of instances did not change and is still running
await _assert_ec2_instances(
ec2_client,
@@ -459,48 +491,28 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
instance_state="running",
)
- # check no rabbit messages were sent
- # NOTE: we currently have no real dask-worker here
- mock_rabbitmq_post_message.assert_not_called()
+ # check rabbit messages were sent
+ mock_rabbitmq_post_message.assert_called()
+ assert mock_rabbitmq_post_message.call_count == num_useless_calls
+ mock_rabbitmq_post_message.reset_mock()
#
# 4. now scaling down, as we deleted all the tasks
#
del dask_future
- # the worker will not be found since there is none in this testing environment, but it should not raise
- await auto_scale_cluster(app=initialized_app, auto_scaling_mode=auto_scaling_mode)
- # check the number of instances did not change and is still running
- await _assert_ec2_instances(
- ec2_client,
- num_reservations=1,
- num_instances=1,
- instance_type=expected_ec2_type,
- instance_state="running",
- )
- # now mock the call so that it triggers deactivation
- mocked_dask_get_worker_still_has_results_in_memory = mocker.patch(
- "simcore_service_autoscaling.modules.dask.get_worker_still_has_results_in_memory",
- return_value=0,
- autospec=True,
- )
- mocked_dask_get_worker_used_resources = mocker.patch(
- "simcore_service_autoscaling.modules.dask.get_worker_used_resources",
- return_value=Resources.create_as_empty(),
- autospec=True,
- )
await auto_scale_cluster(app=initialized_app, auto_scaling_mode=auto_scaling_mode)
- mocked_dask_get_worker_still_has_results_in_memory.assert_called()
- assert mocked_dask_get_worker_still_has_results_in_memory.call_count == 2
- mocked_dask_get_worker_still_has_results_in_memory.reset_mock()
- mocked_dask_get_worker_used_resources.assert_called()
- assert mocked_dask_get_worker_used_resources.call_count == 2
- mocked_dask_get_worker_used_resources.reset_mock()
+ mock_dask_get_worker_has_results_in_memory.assert_called()
+ assert mock_dask_get_worker_has_results_in_memory.call_count == 2
+ mock_dask_get_worker_has_results_in_memory.reset_mock()
+ mock_dask_get_worker_used_resources.assert_called()
+ assert mock_dask_get_worker_used_resources.call_count == 2
+ mock_dask_get_worker_used_resources.reset_mock()
# the node shall be set to drain, but not yet terminated
- mock_set_node_availability.assert_called_once_with(
+ mock_docker_set_node_availability.assert_called_once_with(
get_docker_client(initialized_app), fake_node, available=False
)
- mock_set_node_availability.reset_mock()
+ mock_docker_set_node_availability.reset_mock()
await _assert_ec2_instances(
ec2_client,
num_reservations=1,
@@ -551,6 +563,9 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
instance_state="terminated",
)
+ # this call should never be used in computational mode
+ mock_docker_compute_node_used_resources.assert_not_called()
+
async def test_cluster_does_not_scale_up_if_defined_instance_is_not_allowed(
minimal_configuration: None,
@@ -658,11 +673,11 @@ async def test_cluster_scaling_up_starts_multiple_instances(
initialized_app: FastAPI,
create_dask_task: Callable[[DaskTaskResources], distributed.Future],
ec2_client: EC2Client,
- mock_tag_node: mock.Mock,
+ mock_docker_tag_node: mock.Mock,
scale_up_params: _ScaleUpParams,
mock_rabbitmq_post_message: mock.Mock,
- mock_find_node_with_name: mock.Mock,
- mock_set_node_availability: mock.Mock,
+ mock_docker_find_node_with_name: mock.Mock,
+ mock_docker_set_node_availability: mock.Mock,
dask_spec_local_cluster: distributed.SpecCluster,
):
# we have nothing running now
@@ -697,9 +712,9 @@ async def test_cluster_scaling_up_starts_multiple_instances(
)
# as the new node is already running, but is not yet connected, hence not tagged and drained
- mock_find_node_with_name.assert_not_called()
- mock_tag_node.assert_not_called()
- mock_set_node_availability.assert_not_called()
+ mock_docker_find_node_with_name.assert_not_called()
+ mock_docker_tag_node.assert_not_called()
+ mock_docker_set_node_availability.assert_not_called()
# check rabbit messages were sent
_assert_rabbit_autoscaling_message_sent(
mock_rabbitmq_post_message,
@@ -728,7 +743,7 @@ async def test__deactivate_empty_nodes(
cluster: Callable[..., Cluster],
host_node: DockerNode,
fake_associated_host_instance: AssociatedInstance,
- mock_set_node_availability: mock.Mock,
+ mock_docker_set_node_availability: mock.Mock,
):
# since we have no service running, we expect the passed node to be set to drain
active_cluster = cluster(active_nodes=[fake_associated_host_instance])
@@ -737,7 +752,7 @@ async def test__deactivate_empty_nodes(
)
assert not updated_cluster.active_nodes
assert len(updated_cluster.drained_nodes) == len(active_cluster.active_nodes)
- mock_set_node_availability.assert_called_once_with(
+ mock_docker_set_node_availability.assert_called_once_with(
mock.ANY, host_node, available=False
)
@@ -748,7 +763,7 @@ async def test__deactivate_empty_nodes_with_finished_tasks_should_not_deactivate
cluster: Callable[..., Cluster],
host_node: DockerNode,
fake_associated_host_instance: AssociatedInstance,
- mock_set_node_availability: mock.Mock,
+ mock_docker_set_node_availability: mock.Mock,
create_dask_task: Callable[[DaskTaskResources], distributed.Future],
):
dask_future = create_dask_task({})
@@ -762,7 +777,7 @@ async def test__deactivate_empty_nodes_with_finished_tasks_should_not_deactivate
initialized_app, deepcopy(active_cluster), ComputationalAutoscaling()
)
assert updated_cluster.active_nodes
- mock_set_node_availability.assert_not_called()
+ mock_docker_set_node_availability.assert_not_called()
# now removing the dask_future shall remove the result from the memory
del dask_future
@@ -772,6 +787,6 @@ async def test__deactivate_empty_nodes_with_finished_tasks_should_not_deactivate
)
assert not updated_cluster.active_nodes
assert len(updated_cluster.drained_nodes) == len(active_cluster.active_nodes)
- mock_set_node_availability.assert_called_once_with(
+ mock_docker_set_node_availability.assert_called_once_with(
mock.ANY, host_node, available=False
)
diff --git a/services/autoscaling/tests/unit/test_modules_auto_scaling_dynamic.py b/services/autoscaling/tests/unit/test_modules_auto_scaling_dynamic.py
index 3f4caf35d1d..c81c4968bb7 100644
--- a/services/autoscaling/tests/unit/test_modules_auto_scaling_dynamic.py
+++ b/services/autoscaling/tests/unit/test_modules_auto_scaling_dynamic.py
@@ -446,7 +446,7 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
fake_node: Node,
mock_rabbitmq_post_message: mock.Mock,
mock_find_node_with_name: mock.Mock,
- mock_set_node_availability: mock.Mock,
+ mock_docker_set_node_availability: mock.Mock,
mock_compute_node_used_resources: mock.Mock,
mocker: MockerFixture,
docker_service_imposed_ec2_type: InstanceTypeType | None,
@@ -487,7 +487,7 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
# as the new node is already running, but is not yet connected, hence not tagged and drained
mock_find_node_with_name.assert_not_called()
mock_tag_node.assert_not_called()
- mock_set_node_availability.assert_not_called()
+ mock_docker_set_node_availability.assert_not_called()
mock_compute_node_used_resources.assert_not_called()
# check rabbit messages were sent
_assert_rabbit_autoscaling_message_sent(
@@ -537,10 +537,10 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
available=False,
)
mock_tag_node.reset_mock()
- mock_set_node_availability.assert_called_once_with(
+ mock_docker_set_node_availability.assert_called_once_with(
get_docker_client(initialized_app), fake_node, available=True
)
- mock_set_node_availability.reset_mock()
+ mock_docker_set_node_availability.reset_mock()
# check rabbit messages were sent, we do have worker
assert fake_node.Description
@@ -586,7 +586,7 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
mock_compute_node_used_resources.assert_called()
mock_find_node_with_name.assert_not_called()
mock_tag_node.assert_not_called()
- mock_set_node_availability.assert_not_called()
+ mock_docker_set_node_availability.assert_not_called()
# check the number of instances did not change and is still running
await _assert_ec2_instances(
ec2_client,
@@ -615,17 +615,17 @@ async def test_cluster_scaling_up_and_down( # noqa: PLR0915
instance_type=expected_ec2_type,
instance_state="running",
)
- mock_set_node_availability.assert_called_once_with(
+ mock_docker_set_node_availability.assert_called_once_with(
get_docker_client(initialized_app), fake_node, available=False
)
- mock_set_node_availability.reset_mock()
+ mock_docker_set_node_availability.reset_mock()
# calling again does the exact same
await auto_scale_cluster(app=initialized_app, auto_scaling_mode=auto_scaling_mode)
- mock_set_node_availability.assert_called_once_with(
+ mock_docker_set_node_availability.assert_called_once_with(
get_docker_client(initialized_app), fake_node, available=False
)
- mock_set_node_availability.reset_mock()
+ mock_docker_set_node_availability.reset_mock()
await _assert_ec2_instances(
ec2_client,
num_reservations=1,
@@ -730,7 +730,7 @@ async def test_cluster_scaling_up_starts_multiple_instances(
scale_up_params: _ScaleUpParams,
mock_rabbitmq_post_message: mock.Mock,
mock_find_node_with_name: mock.Mock,
- mock_set_node_availability: mock.Mock,
+ mock_docker_set_node_availability: mock.Mock,
):
# we have nothing running now
all_instances = await ec2_client.describe_instances()
@@ -774,7 +774,7 @@ async def test_cluster_scaling_up_starts_multiple_instances(
# as the new node is already running, but is not yet connected, hence not tagged and drained
mock_find_node_with_name.assert_not_called()
mock_tag_node.assert_not_called()
- mock_set_node_availability.assert_not_called()
+ mock_docker_set_node_availability.assert_not_called()
# check rabbit messages were sent
_assert_rabbit_autoscaling_message_sent(
mock_rabbitmq_post_message,
@@ -791,7 +791,7 @@ async def test__deactivate_empty_nodes(
cluster: Callable[..., Cluster],
host_node: Node,
fake_ec2_instance_data: Callable[..., EC2InstanceData],
- mock_set_node_availability: mock.Mock,
+ mock_docker_set_node_availability: mock.Mock,
):
# since we have no service running, we expect the passed node to be set to drain
active_cluster = cluster(
@@ -802,7 +802,7 @@ async def test__deactivate_empty_nodes(
)
assert not updated_cluster.active_nodes
assert len(updated_cluster.drained_nodes) == len(active_cluster.active_nodes)
- mock_set_node_availability.assert_called_once_with(
+ mock_docker_set_node_availability.assert_called_once_with(
mock.ANY, host_node, available=False
)
@@ -813,7 +813,7 @@ async def test__deactivate_empty_nodes_to_drain_when_services_running_are_missin
cluster: Callable[..., Cluster],
host_node: Node,
fake_ec2_instance_data: Callable[..., EC2InstanceData],
- mock_set_node_availability: mock.Mock,
+ mock_docker_set_node_availability: mock.Mock,
create_service: Callable[
[dict[str, Any], dict[DockerLabelKey, str], str], Awaitable[Service]
],
@@ -838,7 +838,7 @@ async def test__deactivate_empty_nodes_to_drain_when_services_running_are_missin
)
assert not updated_cluster.active_nodes
assert len(updated_cluster.drained_nodes) == len(active_cluster.active_nodes)
- mock_set_node_availability.assert_called_once_with(
+ mock_docker_set_node_availability.assert_called_once_with(
mock.ANY, host_node, available=False
)
@@ -850,7 +850,7 @@ async def test__deactivate_empty_nodes_does_not_drain_if_service_is_running_with
cluster: Callable[..., Cluster],
host_node: Node,
fake_ec2_instance_data: Callable[..., EC2InstanceData],
- mock_set_node_availability: mock.Mock,
+ mock_docker_set_node_availability: mock.Mock,
create_service: Callable[
[dict[str, Any], dict[DockerLabelKey, str], str], Awaitable[Service]
],
@@ -878,7 +878,7 @@ async def test__deactivate_empty_nodes_does_not_drain_if_service_is_running_with
initialized_app, active_cluster, DynamicAutoscaling()
)
assert updated_cluster == active_cluster
- mock_set_node_availability.assert_not_called()
+ mock_docker_set_node_availability.assert_not_called()
async def test__find_terminateable_nodes_with_no_hosts(
diff --git a/services/autoscaling/tests/unit/test_utils_computational_scaling.py b/services/autoscaling/tests/unit/test_utils_computational_scaling.py
index 945b883187e..fa06a5a18b8 100644
--- a/services/autoscaling/tests/unit/test_utils_computational_scaling.py
+++ b/services/autoscaling/tests/unit/test_utils_computational_scaling.py
@@ -14,6 +14,8 @@
from pydantic import ByteSize, parse_obj_as
from pytest_mock import MockerFixture
from simcore_service_autoscaling.models import (
+ AssignedTasksToInstance,
+ AssignedTasksToInstanceType,
AssociatedInstance,
DaskTask,
DaskTaskResources,
@@ -133,7 +135,7 @@ async def test_try_assigning_task_to_node(
assert instance_to_tasks[0][1] == [task, task]
-async def test_try_assigning_task_to_pending_instances_with_no_instances(
+async def test_try_assigning_task_to_instances_with_no_instances(
fake_app: mock.Mock,
fake_task: Callable[..., DaskTask],
):
@@ -144,15 +146,19 @@ async def test_try_assigning_task_to_pending_instances_with_no_instances(
)
-async def test_try_assigning_task_to_pending_instances(
+async def test_try_assigning_task_to_instances(
fake_app: mock.Mock,
fake_task: Callable[..., DaskTask],
fake_ec2_instance_data: Callable[..., EC2InstanceData],
):
task = fake_task(required_resources={"CPU": 2})
ec2_instance = fake_ec2_instance_data()
- pending_instance_to_tasks: list[tuple[EC2InstanceData, list[DaskTask]]] = [
- (ec2_instance, [])
+ pending_instance_to_tasks: list[AssignedTasksToInstance] = [
+ AssignedTasksToInstance(
+ instance=ec2_instance,
+ assigned_tasks=[],
+ available_resources=Resources(cpus=4, ram=ByteSize(1024**2)),
+ )
]
# calling once should allow to add that task to the instance
@@ -165,7 +171,7 @@ async def test_try_assigning_task_to_pending_instances(
)
is True
)
- assert pending_instance_to_tasks[0][1] == [task]
+ assert pending_instance_to_tasks[0].assigned_tasks == [task]
# calling a second time as well should allow to add that task to the instance
assert (
await try_assigning_task_to_instances(
@@ -176,7 +182,7 @@ async def test_try_assigning_task_to_pending_instances(
)
is True
)
- assert pending_instance_to_tasks[0][1] == [task, task]
+ assert pending_instance_to_tasks[0].assigned_tasks == [task, task]
# calling a third time should fail
assert (
await try_assigning_task_to_instances(
@@ -187,7 +193,7 @@ async def test_try_assigning_task_to_pending_instances(
)
is False
)
- assert pending_instance_to_tasks[0][1] == [task, task]
+ assert pending_instance_to_tasks[0].assigned_tasks == [task, task]
def test_try_assigning_task_to_instance_types_with_empty_types(
@@ -205,16 +211,16 @@ def test_try_assigning_task_to_instance_types(
fake_instance_type = EC2InstanceType(
name=faker.name(), cpus=6, ram=parse_obj_as(ByteSize, "2GiB")
)
- instance_type_to_tasks: list[tuple[EC2InstanceType, list[DaskTask]]] = [
- (fake_instance_type, [])
+ instance_type_to_tasks: list[AssignedTasksToInstanceType] = [
+ AssignedTasksToInstanceType(instance_type=fake_instance_type, assigned_tasks=[])
]
# now this should work 3 times
assert try_assigning_task_to_instance_types(task, instance_type_to_tasks) is True
- assert instance_type_to_tasks[0][1] == [task]
+ assert instance_type_to_tasks[0].assigned_tasks == [task]
assert try_assigning_task_to_instance_types(task, instance_type_to_tasks) is True
- assert instance_type_to_tasks[0][1] == [task, task]
+ assert instance_type_to_tasks[0].assigned_tasks == [task, task]
assert try_assigning_task_to_instance_types(task, instance_type_to_tasks) is True
- assert instance_type_to_tasks[0][1] == [task, task, task]
+ assert instance_type_to_tasks[0].assigned_tasks == [task, task, task]
# now it should fail
assert try_assigning_task_to_instance_types(task, instance_type_to_tasks) is False
- assert instance_type_to_tasks[0][1] == [task, task, task]
+ assert instance_type_to_tasks[0].assigned_tasks == [task, task, task]
diff --git a/services/autoscaling/tests/unit/test_utils_dynamic_scaling.py b/services/autoscaling/tests/unit/test_utils_dynamic_scaling.py
index d27bc0dd28f..ab3b3c55e3f 100644
--- a/services/autoscaling/tests/unit/test_utils_dynamic_scaling.py
+++ b/services/autoscaling/tests/unit/test_utils_dynamic_scaling.py
@@ -8,13 +8,14 @@
from datetime import timedelta
import pytest
-from aws_library.ec2.models import EC2InstanceData, EC2InstanceType
+from aws_library.ec2.models import EC2InstanceData, Resources
from faker import Faker
from models_library.generated_models.docker_rest_api import Task
from pydantic import ByteSize
from pytest_mock import MockerFixture
+from simcore_service_autoscaling.models import AssignedTasksToInstance
from simcore_service_autoscaling.utils.dynamic_scaling import (
- try_assigning_task_to_pending_instances,
+ try_assigning_task_to_instances,
)
@@ -28,22 +29,21 @@ def _creator(**overrides) -> Task:
return _creator
-async def test_try_assigning_task_to_pending_instances_with_no_instances(
+async def test_try_assigning_task_to_instances_with_no_instances(
mocker: MockerFixture,
fake_task: Callable[..., Task],
- fake_ec2_instance_data: Callable[..., EC2InstanceData],
):
fake_app = mocker.Mock()
pending_task = fake_task()
assert (
- await try_assigning_task_to_pending_instances(
- fake_app, pending_task, [], {}, notify_progress=True
+ await try_assigning_task_to_instances(
+ fake_app, pending_task, [], notify_progress=True
)
is False
)
-async def test_try_assigning_task_to_pending_instances(
+async def test_try_assigning_task_to_instances(
mocker: MockerFixture,
fake_task: Callable[..., Task],
fake_ec2_instance_data: Callable[..., EC2InstanceData],
@@ -56,43 +56,40 @@ async def test_try_assigning_task_to_pending_instances(
Spec={"Resources": {"Reservations": {"NanoCPUs": 2 * 1e9}}}
)
fake_instance = fake_ec2_instance_data()
- pending_instance_to_tasks: list[tuple[EC2InstanceData, list[Task]]] = [
- (fake_instance, [])
- ]
- type_to_instance_map = {
- fake_instance.type: EC2InstanceType(
- name=fake_instance.type, cpus=4, ram=ByteSize(1024 * 1024)
+ pending_instance_to_tasks: list[AssignedTasksToInstance] = [
+ AssignedTasksToInstance(
+ instance=fake_instance,
+ assigned_tasks=[],
+ available_resources=Resources(cpus=4, ram=ByteSize(1024**3)),
)
- }
+ ]
+
# calling once should allow to add that task to the instance
assert (
- await try_assigning_task_to_pending_instances(
+ await try_assigning_task_to_instances(
fake_app,
pending_task,
pending_instance_to_tasks,
- type_to_instance_map,
notify_progress=True,
)
is True
)
# calling a second time as well should allow to add that task to the instance
assert (
- await try_assigning_task_to_pending_instances(
+ await try_assigning_task_to_instances(
fake_app,
pending_task,
pending_instance_to_tasks,
- type_to_instance_map,
notify_progress=True,
)
is True
)
# calling a third time should fail
assert (
- await try_assigning_task_to_pending_instances(
+ await try_assigning_task_to_instances(
fake_app,
pending_task,
pending_instance_to_tasks,
- type_to_instance_map,
notify_progress=True,
)
is False
From a4d9fecdbc53dc354a5c44fc4a2cb1d82ceab3a9 Mon Sep 17 00:00:00 2001
From: Sylvain <35365065+sanderegg@users.noreply.github.com>
Date: Tue, 14 Nov 2023 17:05:48 +0100
Subject: [PATCH 08/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Clusters-keeper:=20r?=
=?UTF-8?q?enamed=20ENV=20variables=20(#5032)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../data/docker-compose.yml | 8 ++++----
.../src/simcore_service_clusters_keeper/utils/clusters.py | 8 ++++----
services/clusters-keeper/tests/manual/README.md | 8 ++++----
services/docker-compose.yml | 8 ++++----
4 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/services/clusters-keeper/src/simcore_service_clusters_keeper/data/docker-compose.yml b/services/clusters-keeper/src/simcore_service_clusters_keeper/data/docker-compose.yml
index 87f4a6cf641..213aac41821 100644
--- a/services/clusters-keeper/src/simcore_service_clusters_keeper/data/docker-compose.yml
+++ b/services/clusters-keeper/src/simcore_service_clusters_keeper/data/docker-compose.yml
@@ -47,8 +47,8 @@ services:
hostname: "{{.Node.Hostname}}-{{.Service.Name}}-{{.Task.Slot}}"
environment:
DASK_MONITORING_URL: tcp://dask-scheduler:8786
- EC2_ACCESS_KEY_ID: ${EC2_CLUSTERS_KEEPER_ACCESS_KEY_ID}
- EC2_ENDPOINT: ${EC2_CLUSTERS_KEEPER_ENDPOINT}
+ EC2_ACCESS_KEY_ID: ${CLUSTERS_KEEPER_EC2_ACCESS_KEY_ID}
+ EC2_ENDPOINT: ${CLUSTERS_KEEPER_EC2_ENDPOINT}
EC2_INSTANCES_ALLOWED_TYPES: ${WORKERS_EC2_INSTANCES_ALLOWED_TYPES}
EC2_INSTANCES_AMI_ID: ${WORKERS_EC2_INSTANCES_AMI_ID}
EC2_INSTANCES_CUSTOM_BOOT_SCRIPTS: ${WORKERS_EC2_INSTANCES_CUSTOM_BOOT_SCRIPTS}
@@ -59,8 +59,8 @@ services:
EC2_INSTANCES_SECURITY_GROUP_IDS: ${WORKERS_EC2_INSTANCES_SECURITY_GROUP_IDS}
EC2_INSTANCES_SUBNET_ID: ${WORKERS_EC2_INSTANCES_SUBNET_ID}
EC2_INSTANCES_TIME_BEFORE_TERMINATION: ${WORKERS_EC2_INSTANCES_TIME_BEFORE_TERMINATION}
- EC2_REGION_NAME: ${EC2_CLUSTERS_KEEPER_REGION_NAME}
- EC2_SECRET_ACCESS_KEY: ${EC2_CLUSTERS_KEEPER_SECRET_ACCESS_KEY}
+ EC2_REGION_NAME: ${CLUSTERS_KEEPER_EC2_REGION_NAME}
+ EC2_SECRET_ACCESS_KEY: ${CLUSTERS_KEEPER_EC2_SECRET_ACCESS_KEY}
LOG_FORMAT_LOCAL_DEV_ENABLED: 1
LOG_LEVEL: ${LOG_LEVEL:-WARNING}
REDIS_HOST: redis
diff --git a/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/clusters.py b/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/clusters.py
index 64cdf4fcb10..805d793a919 100644
--- a/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/clusters.py
+++ b/services/clusters-keeper/src/simcore_service_clusters_keeper/utils/clusters.py
@@ -40,8 +40,8 @@ def _convert_to_env_list(entries: list[Any]) -> str:
environment_variables = [
f"DOCKER_IMAGE_TAG={app_settings.CLUSTERS_KEEPER_COMPUTATIONAL_BACKEND_DOCKER_IMAGE_TAG}",
- f"EC2_CLUSTERS_KEEPER_ACCESS_KEY_ID={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_ACCESS_KEY_ID}",
- f"EC2_CLUSTERS_KEEPER_ENDPOINT={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_ENDPOINT}",
+ f"CLUSTERS_KEEPER_EC2_ACCESS_KEY_ID={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_ACCESS_KEY_ID}",
+ f"CLUSTERS_KEEPER_EC2_ENDPOINT={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_ENDPOINT}",
f"WORKERS_EC2_INSTANCES_ALLOWED_TYPES={_convert_to_env_list(app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_ALLOWED_TYPES)}",
f"WORKERS_EC2_INSTANCES_AMI_ID={app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_AMI_ID}",
f"WORKERS_EC2_INSTANCES_CUSTOM_BOOT_SCRIPTS={_convert_to_env_list(app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_CUSTOM_BOOT_SCRIPTS)}",
@@ -52,8 +52,8 @@ def _convert_to_env_list(entries: list[Any]) -> str:
f"WORKERS_EC2_INSTANCES_SECURITY_GROUP_IDS={_convert_to_env_list(app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_SECURITY_GROUP_IDS)}",
f"WORKERS_EC2_INSTANCES_SUBNET_ID={app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_SUBNET_ID}",
f"WORKERS_EC2_INSTANCES_TIME_BEFORE_TERMINATION={app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_TIME_BEFORE_TERMINATION}",
- f"EC2_CLUSTERS_KEEPER_REGION_NAME={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_REGION_NAME}",
- f"EC2_CLUSTERS_KEEPER_SECRET_ACCESS_KEY={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_SECRET_ACCESS_KEY}",
+ f"CLUSTERS_KEEPER_EC2_REGION_NAME={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_REGION_NAME}",
+ f"CLUSTERS_KEEPER_EC2_SECRET_ACCESS_KEY={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_SECRET_ACCESS_KEY}",
f"LOG_LEVEL={app_settings.LOG_LEVEL}",
]
diff --git a/services/clusters-keeper/tests/manual/README.md b/services/clusters-keeper/tests/manual/README.md
index 4d508b28226..a2ecd57c8a0 100644
--- a/services/clusters-keeper/tests/manual/README.md
+++ b/services/clusters-keeper/tests/manual/README.md
@@ -60,10 +60,10 @@ S3_SECURE=1
4. prepare clusters-keeper:
```bash
CLUSTERS_KEEPER_EC2_ACCESS={}
-EC2_CLUSTERS_KEEPER_ACCESS_KEY_ID=XXXXXXX
-EC2_CLUSTERS_KEEPER_ENDPOINT=https://ec2.amazonaws.com
-EC2_CLUSTERS_KEEPER_REGION_NAME=us-east-1
-EC2_CLUSTERS_KEEPER_SECRET_ACCESS_KEY=XXXXXXX
+CLUSTERS_KEEPER_EC2_ACCESS_KEY_ID=XXXXXXX
+CLUSTERS_KEEPER_EC2_ENDPOINT=https://ec2.amazonaws.com
+CLUSTERS_KEEPER_EC2_REGION_NAME=us-east-1
+CLUSTERS_KEEPER_SECRET_EC2_ACCESS_KEY=XXXXXXX
CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES={}
PRIMARY_EC2_INSTANCES_ALLOWED_TYPES="[\"t2.micro\"]"
diff --git a/services/docker-compose.yml b/services/docker-compose.yml
index 0d37c5d002d..e8f63bbaaf7 100644
--- a/services/docker-compose.yml
+++ b/services/docker-compose.yml
@@ -86,10 +86,10 @@ services:
- CLUSTERS_KEEPER_TASK_INTERVAL=${CLUSTERS_KEEPER_TASK_INTERVAL}
- CLUSTERS_KEEPER_LOGLEVEL=${LOG_LEVEL:-WARNING}
- CLUSTERS_KEEPER_EC2_ACCESS=${CLUSTERS_KEEPER_EC2_ACCESS}
- - EC2_CLUSTERS_KEEPER_ACCESS_KEY_ID=${EC2_CLUSTERS_KEEPER_ACCESS_KEY_ID}
- - EC2_CLUSTERS_KEEPER_ENDPOINT=${EC2_CLUSTERS_KEEPER_ENDPOINT}
- - EC2_CLUSTERS_KEEPER_REGION_NAME=${EC2_CLUSTERS_KEEPER_REGION_NAME}
- - EC2_CLUSTERS_KEEPER_SECRET_ACCESS_KEY=${EC2_CLUSTERS_KEEPER_SECRET_ACCESS_KEY}
+ - CLUSTERS_KEEPER_EC2_ACCESS_KEY_ID=${CLUSTERS_KEEPER_EC2_ACCESS_KEY_ID}
+ - CLUSTERS_KEEPER_EC2_ENDPOINT=${CLUSTERS_KEEPER_EC2_ENDPOINT}
+ - CLUSTERS_KEEPER_EC2_REGION_NAME=${CLUSTERS_KEEPER_EC2_REGION_NAME}
+ - CLUSTERS_KEEPER_EC2_SECRET_ACCESS_KEY=${CLUSTERS_KEEPER_}
- LOG_FORMAT_LOCAL_DEV_ENABLED=${LOG_FORMAT_LOCAL_DEV_ENABLED}
- CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES=${CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES}
- PRIMARY_EC2_INSTANCES_ALLOWED_TYPES=${PRIMARY_EC2_INSTANCES_ALLOWED_TYPES}
From cd84f2fbedc618776ffc73e39e30c1cbaeeb7fde Mon Sep 17 00:00:00 2001
From: Julian Querido
Date: Wed, 15 Nov 2023 10:27:49 +0100
Subject: [PATCH 09/24] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20Perform=20experiment?=
=?UTF-8?q?s=20-=20MarkerIO=20(#5030)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
services/static-webserver/client/source/boot/index.html | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/services/static-webserver/client/source/boot/index.html b/services/static-webserver/client/source/boot/index.html
index 014f74b544d..0452a911a4b 100644
--- a/services/static-webserver/client/source/boot/index.html
+++ b/services/static-webserver/client/source/boot/index.html
@@ -56,4 +56,12 @@
${preBootJs}