From b11585947d4bce4000369e374090f9cebfa09f3f Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Thu, 19 Dec 2024 22:05:32 +0500 Subject: [PATCH 01/16] ADCM-6210 Prepare for Job tests --- adcm_aio_client/core/actions/_objects.py | 38 +++++++++-- adcm_aio_client/core/filters.py | 5 +- adcm_aio_client/core/host_groups/_common.py | 3 +- .../core/host_groups/action_group.py | 9 +-- adcm_aio_client/core/objects/cm.py | 67 +++++++++++++++++-- .../bundles/complex_cluster/actions.yaml | 0 .../bundles/complex_cluster/config.yaml | 47 +++++++++++++ .../bundles/simple_hostprovider/config.yaml | 13 ++++ tests/integration/conftest.py | 14 ++++ 9 files changed, 174 insertions(+), 22 deletions(-) create mode 100644 tests/integration/bundles/complex_cluster/actions.yaml diff --git a/adcm_aio_client/core/actions/_objects.py b/adcm_aio_client/core/actions/_objects.py index bf0c11f..78e2ddf 100644 --- a/adcm_aio_client/core/actions/_objects.py +++ b/adcm_aio_client/core/actions/_objects.py @@ -6,12 +6,13 @@ from asyncstdlib import cached_property as async_cached_property from adcm_aio_client.core.errors import HostNotInClusterError, NoMappingRulesForActionError +from adcm_aio_client.core.filters import FilterByDisplayName, FilterByName, Filtering from adcm_aio_client.core.mapping import ActionMapping from adcm_aio_client.core.objects._accessors import NonPaginatedChildAccessor from adcm_aio_client.core.objects._base import InteractiveChildObject, InteractiveObject if TYPE_CHECKING: - from adcm_aio_client.core.objects.cm import Bundle, Cluster + from adcm_aio_client.core.objects.cm import Bundle, Cluster, Job class Action(InteractiveChildObject): @@ -20,6 +21,25 @@ class Action(InteractiveChildObject): def __init__(self: Self, parent: InteractiveObject, data: dict[str, Any]) -> None: super().__init__(parent, data) self._verbose = False + self._blocking = True + + @property + def verbose(self: Self) -> bool: + return self._verbose + + @verbose.setter + def verbose(self: Self, value: bool) -> bool: + self._verbose = value + return self._verbose + + @property + def blocking(self: Self) -> bool: + return self._blocking + + @blocking.setter + def blocking(self: Self, value: bool) -> bool: + self._blocking = value + return self._blocking @cached_property def name(self: Self) -> str: @@ -29,8 +49,14 @@ def name(self: Self) -> str: def display_name(self: Self) -> str: return self._data["displayName"] - async def run(self: Self) -> dict: # TODO: implement Task, return Task - return (await self._requester.post(*self.get_own_path(), "run", data={"isVerbose": self._verbose})).as_dict() + async def run(self: Self) -> Job: + from adcm_aio_client.core.objects.cm import Job + + # todo build data for config and mapping + data = {"isVerbose": self._verbose, "isBlocking": self._blocking} + response = await self._requester.post(*self.get_own_path(), "run", data=data) + job = Job(requester=self._requester, data=response.as_dict()) + return job @async_cached_property async def _mapping_rule(self: Self) -> list[dict] | None: @@ -49,10 +75,6 @@ async def mapping(self: Self) -> ActionMapping: return ActionMapping(owner=self._parent, cluster=cluster, entries=entries) - def set_verbose(self: Self) -> Self: - self._verbose = True - return self - @async_cached_property # TODO: Config class async def config(self: Self) -> ...: return (await self._rich_data)["configuration"] @@ -64,6 +86,7 @@ async def _rich_data(self: Self) -> dict: class ActionsAccessor(NonPaginatedChildAccessor): class_type = Action + filtering = Filtering(FilterByName, FilterByDisplayName) class Upgrade(Action): @@ -85,6 +108,7 @@ def validate(self: Self) -> bool: class UpgradeNode(NonPaginatedChildAccessor): class_type = Upgrade + filtering = Filtering(FilterByName, FilterByDisplayName) async def detect_cluster(owner: InteractiveObject) -> Cluster: diff --git a/adcm_aio_client/core/filters.py b/adcm_aio_client/core/filters.py index eb7c5f2..178ef30 100644 --- a/adcm_aio_client/core/filters.py +++ b/adcm_aio_client/core/filters.py @@ -24,7 +24,8 @@ COMMON_OPERATIONS = frozenset(("eq", "ne", "in", "exclude")) -ALL_OPERATIONS = frozenset(("contains", "icontains", *COMMON_OPERATIONS, *tuple(f"i{op}" for op in COMMON_OPERATIONS))) +STATUS_OPERATIONS = frozenset((*COMMON_OPERATIONS, *tuple(f"i{op}" for op in COMMON_OPERATIONS))) +ALL_OPERATIONS = frozenset(("contains", "icontains", *STATUS_OPERATIONS)) type FilterSingleValue = str | int | InteractiveObject type FilterValue = FilterSingleValue | Iterable[FilterSingleValue] @@ -158,4 +159,4 @@ def _prepare_query_param_value(self: Self, value: SimplifiedValue) -> str: FilterByName = FilterBy("name", ALL_OPERATIONS, str) FilterByDisplayName = FilterBy("display_name", ALL_OPERATIONS, str) -FilterByStatus = FilterBy("status", COMMON_OPERATIONS, str) +FilterByStatus = FilterBy("status", STATUS_OPERATIONS, str) diff --git a/adcm_aio_client/core/host_groups/_common.py b/adcm_aio_client/core/host_groups/_common.py index 185c726..7cf8eee 100644 --- a/adcm_aio_client/core/host_groups/_common.py +++ b/adcm_aio_client/core/host_groups/_common.py @@ -10,7 +10,6 @@ PaginatedChildAccessor, filters_to_inline, ) -from adcm_aio_client.core.objects._base import InteractiveChildObject from adcm_aio_client.core.types import Endpoint, HostID, QueryParameters, Requester, RequesterResponse from adcm_aio_client.core.utils import safe_gather @@ -99,7 +98,7 @@ class HostGroupNode[ ](PaginatedChildAccessor[Parent, Child]): async def create( # TODO: can create HG with subset of `hosts` if adding some of them leads to an error self: Self, name: str, description: str = "", hosts: list["Host"] | None = None - ) -> InteractiveChildObject: + ) -> Child: response = await self._requester.post(*self._path, data={"name": name, "description": description}) host_group = self.class_type(parent=self._parent, data=response.as_dict()) diff --git a/adcm_aio_client/core/host_groups/action_group.py b/adcm_aio_client/core/host_groups/action_group.py index 13b3900..63f4897 100644 --- a/adcm_aio_client/core/host_groups/action_group.py +++ b/adcm_aio_client/core/host_groups/action_group.py @@ -1,17 +1,16 @@ from functools import cached_property from typing import TYPE_CHECKING, Self, Union -from adcm_aio_client.core.actions import ActionsAccessor from adcm_aio_client.core.host_groups._common import HostGroupNode, HostsInHostGroupNode from adcm_aio_client.core.objects._base import InteractiveChildObject -from adcm_aio_client.core.objects._common import Deletable +from adcm_aio_client.core.objects._common import Deletable, WithActions from adcm_aio_client.core.types import AwareOfOwnPath, WithProtectedRequester if TYPE_CHECKING: from adcm_aio_client.core.objects.cm import Cluster, Component, Service -class ActionHostGroup(InteractiveChildObject, Deletable): +class ActionHostGroup(InteractiveChildObject, WithActions, Deletable): PATH_PREFIX = "action-host-groups" @property @@ -26,10 +25,6 @@ def description(self: Self) -> str: def hosts(self: Self) -> "HostsInActionHostGroupNode": return HostsInActionHostGroupNode(path=(*self.get_own_path(), "hosts"), requester=self._requester) - @cached_property - def actions(self: Self) -> ActionsAccessor: - return ActionsAccessor(parent=self, path=(*self.get_own_path(), "actions"), requester=self._requester) - class ActionHostGroupNode(HostGroupNode[Union["Cluster", "Service", "Component"], ActionHostGroup]): class_type = ActionHostGroup diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index c2e6151..715db16 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -2,13 +2,13 @@ from datetime import datetime, timedelta from functools import cached_property from pathlib import Path -from typing import Any, Awaitable, Callable, Iterable, Literal, Self +from typing import Any, AsyncGenerator, Awaitable, Callable, Iterable, Literal, Self import asyncio from asyncstdlib.functools import cached_property as async_cached_property # noqa: N813 from adcm_aio_client.core.actions._objects import Action -from adcm_aio_client.core.errors import NotFoundError +from adcm_aio_client.core.errors import InvalidFilterError, NotFoundError from adcm_aio_client.core.filters import ( ALL_OPERATIONS, COMMON_OPERATIONS, @@ -18,8 +18,10 @@ FilterByName, FilterByStatus, Filtering, + FilterValue, ) from adcm_aio_client.core.host_groups import WithActionHostGroups, WithConfigHostGroups +from adcm_aio_client.core.host_groups.action_group import ActionHostGroup from adcm_aio_client.core.mapping import ClusterMapping from adcm_aio_client.core.objects._accessors import ( PaginatedAccessor, @@ -470,11 +472,12 @@ def name(self: Self) -> str: return str(self._data["name"]) @property - def start_time(self: Self) -> datetime: + def start_time(self: Self) -> datetime | None: + # todo test it that it should be datetime, not str return self._data["startTime"] @property - def finish_time(self: Self) -> datetime: + def finish_time(self: Self) -> datetime | None: return self._data["endTime"] @property @@ -513,3 +516,59 @@ async def wait( async def terminate(self: Self) -> None: await self._requester.post(*self.get_own_path(), "terminate", data={}) + + +class JobsNode(PaginatedAccessor[Job]): + class_type = Job + filtering = Filtering( + FilterByName, + FilterByDisplayName, + FilterByStatus, + FilterBy("action", COMMON_OPERATIONS, Action), + # technical filters, don't use them directly + FilterBy("target_id", ("eq",), int), + FilterBy("target_type", ("eq",), str), + ) + + # override accessor methods to allow passing object + + async def get(self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue) -> Job: + object_filter = self._prepare_filter_by_object(object) + all_filters = filters | object_filter + return await super().get(**all_filters) + + async def get_or_none(self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue) -> Job | None: + object_filter = self._prepare_filter_by_object(object) + all_filters = filters | object_filter + return await super().get(**all_filters) + + async def filter(self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue) -> list[Job]: + object_filter = self._prepare_filter_by_object(object) + all_filters = filters | object_filter + return await super().filter(**all_filters) + + async def iter( + self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue + ) -> AsyncGenerator[Job, None]: + object_filter = self._prepare_filter_by_object(object) + all_filters = filters | object_filter + async for entry in super().iter(**all_filters): + yield entry + + def _prepare_filter_by_object(self: Self, object_: InteractiveObject | None) -> dict: + if object_ is None: + return {} + + object_id = object_.id + + if isinstance(object_, (Cluster, Service, Component, Host)): + object_type = object_.__class__.__name__.lower() + elif isinstance(object_, HostProvider): + object_type = "provider" + elif isinstance(object_, ActionHostGroup): + object_type = "action-host-group" + else: + message = f"Failed to build filter: {object_.__class__.__name__} " "can't be an owner of Job" + raise InvalidFilterError(message) + + return {"target_id__eq": object_id, "target_type__eq": object_type} diff --git a/tests/integration/bundles/complex_cluster/actions.yaml b/tests/integration/bundles/complex_cluster/actions.yaml new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/bundles/complex_cluster/config.yaml b/tests/integration/bundles/complex_cluster/config.yaml index 02ebca0..d76f9c9 100644 --- a/tests/integration/bundles/complex_cluster/config.yaml +++ b/tests/integration/bundles/complex_cluster/config.yaml @@ -2,6 +2,41 @@ name: Some Cluster version: 1 + actions: &actions + success: &job + display_name: I will survive + type: job + script_type: ansible + script: ./actions.yaml + allow_to_terminate: true + allow_for_action_host_group: true + params: + ansible_tags: ok + masking: {} + + fail: + <<: *job + display_name: no Way + params: + ansible_tags: fail + + success_task: + display_name: Lots Of me + type: task + masking: {} + allow_to_terminate: true + allow_for_action_host_group: true + scripts: + - &success_job + name: first + script_type: ansible + script: ./actions.yaml + params: + ansible_tags: ok + - <<: *success_job + name: second + display_name: AnothEr + - type: service name: example_1 display_name: First Example @@ -27,6 +62,18 @@ components: *example_c +- type: service + name: with_actions + version: 2.3 + actions: *actions + + components: + c1: + display_name: Awesome + actions: *actions + c2: + actions: *actions + - type: service name: complex_config version: 0.3 diff --git a/tests/integration/bundles/simple_hostprovider/config.yaml b/tests/integration/bundles/simple_hostprovider/config.yaml index 5662876..12e0f7f 100644 --- a/tests/integration/bundles/simple_hostprovider/config.yaml +++ b/tests/integration/bundles/simple_hostprovider/config.yaml @@ -2,6 +2,19 @@ name: simple_provider version: 4 + actions: &actions + success: &job + display_name: I will survive + type: job + script_type: ansible + script: ./actions.yaml + params: + ansible_tags: ok + masking: {} + + - type: host name: simple_host version: 2 + + actions: *actions diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index cfbcbcc..af97567 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -9,7 +9,9 @@ from adcm_aio_client._session import ADCMSession from adcm_aio_client.core.client import ADCMClient +from adcm_aio_client.core.objects.cm import Bundle from adcm_aio_client.core.types import Credentials +from tests.integration.bundle import pack_bundle from tests.integration.setup_environment import ( DB_USER, ADCMContainer, @@ -52,3 +54,15 @@ async def adcm_client(adcm: ADCMContainer) -> AsyncGenerator[ADCMClient, None]: url = adcm.url async with ADCMSession(url=url, credentials=credentials, timeout=10, retry_interval=1, retry_attempts=1) as client: yield client + + +@pytest_asyncio.fixture() +async def complex_cluster_bundle(adcm_client: ADCMClient, tmp_path: Path) -> Bundle: + bundle_path = pack_bundle(from_dir=BUNDLES / "complex_cluster", to=tmp_path) + return await adcm_client.bundles.create(source=bundle_path, accept_license=True) + + +@pytest_asyncio.fixture() +async def simple_hostprovider_bundle(adcm_client: ADCMClient, tmp_path: Path) -> Bundle: + bundle_path = pack_bundle(from_dir=BUNDLES / "simple_hostprovider", to=tmp_path) + return await adcm_client.bundles.create(source=bundle_path, accept_license=True) From fee608f01bc5d816a47cd87cfdeb381e0fdba428 Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Fri, 20 Dec 2024 15:49:26 +0500 Subject: [PATCH 02/16] ADCM-6210 Test and Job fixes --- adcm_aio_client/core/actions/_objects.py | 7 +- adcm_aio_client/core/objects/_base.py | 8 +- adcm_aio_client/core/objects/_common.py | 1 - adcm_aio_client/core/objects/cm.py | 112 +++++++++++----- .../bundles/complex_cluster/actions.yaml | 16 +++ tests/integration/setup_environment.py | 2 +- tests/integration/test_jobs.py | 126 ++++++++++++++++++ 7 files changed, 232 insertions(+), 40 deletions(-) create mode 100644 tests/integration/test_jobs.py diff --git a/adcm_aio_client/core/actions/_objects.py b/adcm_aio_client/core/actions/_objects.py index 78e2ddf..f0b3686 100644 --- a/adcm_aio_client/core/actions/_objects.py +++ b/adcm_aio_client/core/actions/_objects.py @@ -53,10 +53,9 @@ async def run(self: Self) -> Job: from adcm_aio_client.core.objects.cm import Job # todo build data for config and mapping - data = {"isVerbose": self._verbose, "isBlocking": self._blocking} + data = {"isVerbose": self._verbose, "shouldBlockObject": self._blocking} response = await self._requester.post(*self.get_own_path(), "run", data=data) - job = Job(requester=self._requester, data=response.as_dict()) - return job + return Job(requester=self._requester, data=response.as_dict()) @async_cached_property async def _mapping_rule(self: Self) -> list[dict] | None: @@ -84,7 +83,7 @@ async def _rich_data(self: Self) -> dict: return (await self._requester.get(*self.get_own_path())).as_dict() -class ActionsAccessor(NonPaginatedChildAccessor): +class ActionsAccessor[Parent: InteractiveObject](NonPaginatedChildAccessor[Parent, Action]): class_type = Action filtering = Filtering(FilterByName, FilterByDisplayName) diff --git a/adcm_aio_client/core/objects/_base.py b/adcm_aio_client/core/objects/_base.py index f1acfb9..c2e8f06 100644 --- a/adcm_aio_client/core/objects/_base.py +++ b/adcm_aio_client/core/objects/_base.py @@ -81,8 +81,6 @@ def _repr(self: Self) -> str: class RootInteractiveObject(InteractiveObject): - PATH_PREFIX: str - def get_own_path(self: Self) -> Endpoint: # change here return self._build_own_path(self.id) @@ -106,6 +104,12 @@ def __init__(self: Self, parent: Parent, data: dict[str, Any]) -> None: def get_own_path(self: Self) -> Endpoint: return *self._parent.get_own_path(), self.PATH_PREFIX, self.id + @classmethod + async def with_id(cls: type[Self], parent: Parent, object_id: int) -> Self: + object_path = (*parent.get_own_path(), cls.PATH_PREFIX, str(object_id)) + response = await parent.requester.get(*object_path) + return cls(parent=parent, data=response.as_dict()) + class MaintenanceMode: def __init__( diff --git a/adcm_aio_client/core/objects/_common.py b/adcm_aio_client/core/objects/_common.py index e51d7b4..ff75c2f 100644 --- a/adcm_aio_client/core/objects/_common.py +++ b/adcm_aio_client/core/objects/_common.py @@ -27,7 +27,6 @@ def actions(self: Self) -> ActionsAccessor: return ActionsAccessor(parent=self, path=(*self.get_own_path(), "actions"), requester=self._requester) -# todo whole section lacking implementation (and maybe code move is required) class WithConfig(ConfigOwner): @cached_property async def config(self: Self) -> ObjectConfig: diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 715db16..bbb5f1a 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -460,63 +460,108 @@ async def _get_hosts( return tuple(hosts) -def default_exit_condition(job: "Job") -> bool: - return job.get_status() in DEFAULT_JOB_TERMINAL_STATUSES +async def default_exit_condition(job: "Job") -> bool: + return await job.get_status() in DEFAULT_JOB_TERMINAL_STATUSES -class Job[Object: "InteractiveObject"](WithStatus, WithActions, RootInteractiveObject): +class Job(WithStatus, RootInteractiveObject): PATH_PREFIX = "tasks" @property def name(self: Self) -> str: return str(self._data["name"]) - @property + @cached_property def start_time(self: Self) -> datetime | None: - # todo test it that it should be datetime, not str - return self._data["startTime"] + time = self._data["startTime"] + if time is None: + return time - @property - def finish_time(self: Self) -> datetime | None: - return self._data["endTime"] + return datetime.fromisoformat(time) - @property - def object(self: Self) -> Object: - obj_data = self._data["objects"][0] - obj_type = obj_data["type"] + @cached_property + def finish_time(self: Self) -> datetime | None: + time = self._data["endTime"] + if time is None: + return time - obj_dict = { - "host": Host, - "component": Component, - "provider": HostProvider, - "cluster": Cluster, - "service": Service, - "adcm": ADCM, - } + return datetime.fromisoformat(time) - return self._construct(what=obj_dict[obj_type], from_data=obj_data) + @async_cached_property + async def object(self: Self) -> InteractiveObject: + objects_raw = self._parse_objects() + return await self._retrieve_target(objects_raw) - @property - def action(self: Self) -> Action: - return self._construct(what=Action, from_data=self._data["action"]) + @async_cached_property + async def action(self: Self) -> Action: + target = await self.object + return Action(parent=target, data=self._data["action"]) async def wait( self: Self, timeout: int | None = None, poll_interval: int = 10, - exit_condition: Callable[[Self], bool] = default_exit_condition, + exit_condition: Callable[[Self], Awaitable[bool]] = default_exit_condition, ) -> Self: timeout_condition = datetime.max if timeout is None else (datetime.now() + timedelta(seconds=timeout)) # noqa: DTZ005 + while datetime.now() < timeout_condition: # noqa: DTZ005 - if exit_condition(self): + if await exit_condition(self): return self + await asyncio.sleep(poll_interval) - raise TimeoutError + message = "Failed to meet exit condition for job" + if timeout: + message = f"{message} in {timeout} seconds with {poll_interval} second interval" + + raise TimeoutError(message) async def terminate(self: Self) -> None: await self._requester.post(*self.get_own_path(), "terminate", data={}) + def _parse_objects(self: Self) -> dict[str, int]: + return {entry["type"]: entry["id"] for entry in self._data["objects"]} + + async def _retrieve_target(self: Self, objects: dict[str, int]) -> InteractiveObject: + match objects: + case {"action_host_group": id_}: + objects.pop("action_host_group") + owner = await self._retrieve_target(objects) + return await ActionHostGroup.with_id(parent=owner, object_id=id_) + + case {"host": id_}: + return await Host.with_id(requester=self._requester, object_id=id_) + + case {"component": id_}: + objects.pop("component") + + owner = await self._retrieve_target(objects) + if not isinstance(owner, Service): + message = f"Incorrect owner for component detected from job data: {owner}" + raise TypeError(message) + + return await Component.with_id(parent=owner, object_id=id_) + + case {"service": id_}: + objects.pop("service") + + owner = await self._retrieve_target(objects) + if not isinstance(owner, Cluster): + message = f"Incorrect owner for service detected from job data: {owner}" + raise TypeError(message) + + return await Service.with_id(parent=owner, object_id=id_) + + case {"cluster": id_}: + return await Cluster.with_id(requester=self._requester, object_id=id_) + + case {"provider": id_}: + return await HostProvider.with_id(requester=self._requester, object_id=id_) + case _: + message = f"Failed to detect Job's owner based on {objects}" + raise RuntimeError(message) + class JobsNode(PaginatedAccessor[Job]): class_type = Job @@ -532,23 +577,26 @@ class JobsNode(PaginatedAccessor[Job]): # override accessor methods to allow passing object - async def get(self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue) -> Job: + async def get(self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue) -> Job: # noqa: A002 object_filter = self._prepare_filter_by_object(object) all_filters = filters | object_filter return await super().get(**all_filters) - async def get_or_none(self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue) -> Job | None: + async def get_or_none(self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue) -> Job | None: # noqa: A002 object_filter = self._prepare_filter_by_object(object) all_filters = filters | object_filter return await super().get(**all_filters) - async def filter(self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue) -> list[Job]: + async def filter(self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue) -> list[Job]: # noqa: A002 object_filter = self._prepare_filter_by_object(object) all_filters = filters | object_filter return await super().filter(**all_filters) async def iter( - self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue + self: Self, + *, + object: InteractiveObject | None = None, # noqa: A002 + **filters: FilterValue, ) -> AsyncGenerator[Job, None]: object_filter = self._prepare_filter_by_object(object) all_filters = filters | object_filter diff --git a/tests/integration/bundles/complex_cluster/actions.yaml b/tests/integration/bundles/complex_cluster/actions.yaml index e69de29..95d47d2 100644 --- a/tests/integration/bundles/complex_cluster/actions.yaml +++ b/tests/integration/bundles/complex_cluster/actions.yaml @@ -0,0 +1,16 @@ +- name: letsgo + hosts: localhost + connection: local + gather_facts: no + + tasks: + - name: Success + debug: + msg: "successful step" + tags: [ok] + + - name: Fail + fail: + msg: "failed step" + tags: [fail] + diff --git a/tests/integration/setup_environment.py b/tests/integration/setup_environment.py index b0a8c63..1b7d519 100644 --- a/tests/integration/setup_environment.py +++ b/tests/integration/setup_environment.py @@ -9,7 +9,7 @@ from testcontainers.postgres import DbContainer, PostgresContainer postgres_image_name = "postgres:latest" -adcm_image_name = "hub.adsw.io/adcm/adcm:feature_ADCM-6181" +adcm_image_name = "hub.adsw.io/adcm/adcm:develop" adcm_container_name = "test_adcm" postgres_name = "test_pg_db" diff --git a/tests/integration/test_jobs.py b/tests/integration/test_jobs.py new file mode 100644 index 0000000..3eb1e28 --- /dev/null +++ b/tests/integration/test_jobs.py @@ -0,0 +1,126 @@ +from datetime import datetime +from itertools import chain +import asyncio + +import pytest +import pytest_asyncio + +from adcm_aio_client.core.client import ADCMClient +from adcm_aio_client.core.filters import Filter, FilterValue +from adcm_aio_client.core.objects._common import WithActions +from adcm_aio_client.core.objects.cm import Bundle, Cluster, Component, Job + +pytestmark = [pytest.mark.asyncio] + + +async def run_non_blocking(target: WithActions, **filters: FilterValue) -> Job: + action = await target.actions.get(**filters) + action.blocking = False + return await action.run() + + +@pytest_asyncio.fixture() +async def prepare_environment( + adcm_client: ADCMClient, + complex_cluster_bundle: Bundle, + simple_hostprovider_bundle: Bundle, +) -> list[Job]: + cluster_bundle = complex_cluster_bundle + hostprovider_bundle = simple_hostprovider_bundle + + clusters: list[Cluster] = await asyncio.gather( + *(adcm_client.clusters.create(cluster_bundle, f"wow-{i}") for i in range(5)) + ) + hostproviders = await asyncio.gather( + *(adcm_client.hostproviders.create(hostprovider_bundle, f"yay-{i}") for i in range(5)) + ) + await asyncio.gather( + *(adcm_client.hosts.create(hp, f"host-{hp.name}-{i}") for i in range(5) for hp in hostproviders) + ) + hosts = await adcm_client.hosts.all() + + services = tuple( + chain.from_iterable( + await asyncio.gather( + *(cluster.services.add(Filter(attr="name", op="eq", value="with_actions")) for cluster in clusters) + ) + ) + ) + components = tuple(chain.from_iterable(await asyncio.gather(*(service.components.all() for service in services)))) + + host_groups = await asyncio.gather( + *( + object_.action_host_groups.create(name=f"ahg for {object_.__class__.__name__}") + for object_ in chain(clusters, services, components) + ) + ) + + object_jobs = asyncio.gather( + *( + run_non_blocking(object_, name__eq="success") + for object_ in chain(clusters, services, components, hosts, hostproviders) + ) + ) + + group_jobs = asyncio.gather(*(run_non_blocking(group, name__in=["fail"]) for group in host_groups)) + + return await object_jobs + await group_jobs + + +@pytest.mark.usefixtures("prepare_environment") +async def test_jobs_api(adcm_client: ADCMClient) -> None: + await _test_basic_api(adcm_client) + await _test_job_object(adcm_client) + await _test_collection_fitlering(adcm_client) + + +async def is_running(job: Job) -> bool: + return await job.get_status() == "running" + + +async def _test_basic_api(adcm_client: ADCMClient) -> None: + # fields: id, name, display_name, start_time, finish_time + # properties: object, action + # methods: get_status, wait, terminate + # refresh (with wait) + cluster, *_ = await adcm_client.clusters.list(query={"limit": 1, "offset": 3}) + service = await cluster.services.get(name__contains="action") + component = await service.components.get(display_name__icontains="wESo") + + action = await component.actions.get(display_name__ieq="Lots of me") + job = await action.run() + # depending on retrieval time it's "one of" + assert await job.get_status() in ("created", "running") + assert job.start_time is None + assert job.finish_time is None + assert (await job.action).id == action.id + + await job.wait(exit_condition=is_running, timeout=5, poll_interval=1) + assert job.start_time is None + await job.refresh() + assert isinstance(job.start_time, datetime) + assert job.finish_time is None + + target = await job.object + assert isinstance(target, Component) + assert target.id == component.id + assert target.service.id == component.service.id + + await job.wait(timeout=10, poll_interval=1) + + assert await job.get_status() == "success" + assert job.finish_time is None + await job.refresh() + assert isinstance(job.finish_time, datetime) + + +async def _test_job_object(adcm_client: ADCMClient) -> None: + # filter by object + # detect object from Job + ... + + +async def _test_collection_fitlering(adcm_client: ADCMClient) -> None: + # filters: status, name, display_name, action + # special filter: object + ... From e33e871a2eaa73c695f949eda8bb6a57c622a188 Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Fri, 20 Dec 2024 18:19:42 +0500 Subject: [PATCH 03/16] ADCM-6210 Add implementation to action.mapping and action.config --- adcm_aio_client/core/actions/_objects.py | 90 ++++++++++++++++++------ adcm_aio_client/core/config/_objects.py | 52 ++++++++------ adcm_aio_client/core/errors.py | 7 +- adcm_aio_client/core/objects/_common.py | 4 +- 4 files changed, 105 insertions(+), 48 deletions(-) diff --git a/adcm_aio_client/core/actions/_objects.py b/adcm_aio_client/core/actions/_objects.py index f0b3686..5ce7519 100644 --- a/adcm_aio_client/core/actions/_objects.py +++ b/adcm_aio_client/core/actions/_objects.py @@ -5,7 +5,9 @@ from asyncstdlib import cached_property as async_cached_property -from adcm_aio_client.core.errors import HostNotInClusterError, NoMappingRulesForActionError +from adcm_aio_client.core.config._objects import ActionConfig +from adcm_aio_client.core.config.types import ConfigData +from adcm_aio_client.core.errors import HostNotInClusterError, NoConfigInActionError, NoMappingInActionError from adcm_aio_client.core.filters import FilterByDisplayName, FilterByName, Filtering from adcm_aio_client.core.mapping import ActionMapping from adcm_aio_client.core.objects._accessors import NonPaginatedChildAccessor @@ -52,21 +54,24 @@ def display_name(self: Self) -> str: async def run(self: Self) -> Job: from adcm_aio_client.core.objects.cm import Job - # todo build data for config and mapping data = {"isVerbose": self._verbose, "shouldBlockObject": self._blocking} + if self._has_mapping: + mapping = await self.mapping + data |= {"hostComponentMap": mapping._to_payload()} + if self._has_config: + config = await self.config + data |= {"configuration": config._to_payload()} + response = await self._requester.post(*self.get_own_path(), "run", data=data) return Job(requester=self._requester, data=response.as_dict()) - @async_cached_property - async def _mapping_rule(self: Self) -> list[dict] | None: - return (await self._rich_data)["hostComponentMapRules"] - @async_cached_property async def mapping(self: Self) -> ActionMapping: - mapping_change_allowed = await self._mapping_rule - if not mapping_change_allowed: + await self._ensure_rich_data() + + if not self._has_mapping: message = f"Action {self.display_name} doesn't allow mapping changes" - raise NoMappingRulesForActionError(message) + raise NoMappingInActionError(message) cluster = await detect_cluster(owner=self._parent) mapping = await cluster.mapping @@ -74,13 +79,61 @@ async def mapping(self: Self) -> ActionMapping: return ActionMapping(owner=self._parent, cluster=cluster, entries=entries) - @async_cached_property # TODO: Config class - async def config(self: Self) -> ...: - return (await self._rich_data)["configuration"] - @async_cached_property - async def _rich_data(self: Self) -> dict: - return (await self._requester.get(*self.get_own_path())).as_dict() + async def config(self: Self) -> ActionConfig: + await self._ensure_rich_data() + + if not self._has_config: + message = f"Action {self.display_name} doesn't allow config changes" + raise NoConfigInActionError(message) + + configuration = self._configuration + data = ConfigData.from_v2_response(data_in_v2_format=configuration) + schema = configuration["configSchema"] + + return ActionConfig(schema=schema, config=data, parent=self) + + @property + def _is_full_data_loaded(self: Self) -> bool: + return "hostComponentMapRules" in self._data + + @property + def _has_mapping(self: Self) -> bool: + return bool(self._mapping_rule) + + @property + def _has_config(self: Self) -> bool: + return bool(self._configuration) + + @property + def _mapping_rule(self: Self) -> list[dict]: + try: + return self._data["hostComponentMapRules"] + except KeyError as e: + message = ( + "Failed to retrieve mapping rules. " + "Most likely action was initialized with partial data." + " Need to load all data" + ) + raise KeyError(message) from e + + @property + def _configuration(self: Self) -> dict: + try: + return self._data["configuration"] + except KeyError as e: + message = ( + "Failed to retrieve configuration section. " + "Most likely action was initialized with partial data." + " Need to load all data" + ) + raise KeyError(message) from e + + async def _ensure_rich_data(self: Self) -> None: + if self._is_full_data_loaded: + return + + self._data = await self._retrieve_data() class ActionsAccessor[Parent: InteractiveObject](NonPaginatedChildAccessor[Parent, Action]): @@ -97,13 +150,6 @@ def bundle(self: Self) -> Bundle: return Bundle(requester=self._requester, data=self._data["bundle"]) - @async_cached_property # TODO: Config class - async def config(self: Self) -> ...: - return (await self._rich_data)["configuration"] - - def validate(self: Self) -> bool: - return True - class UpgradeNode(NonPaginatedChildAccessor): class_type = Upgrade diff --git a/adcm_aio_client/core/config/_objects.py b/adcm_aio_client/core/config/_objects.py index c36773a..a8463ab 100644 --- a/adcm_aio_client/core/config/_objects.py +++ b/adcm_aio_client/core/config/_objects.py @@ -301,26 +301,6 @@ def difference(self: Self, other: Self, *, other_is_previous: bool = True) -> Co full_diff = find_config_difference(previous=previous.data, current=current.data, schema=self._schema) return ConfigDifference.from_full_format(full_diff) - async def save(self: Self, description: str = "") -> Self: - config_to_save = self._current_config.config - self._serialize_json_fields_inplace_safe(config_to_save) - payload = {"description": description, "config": config_to_save.values, "adcmMeta": config_to_save.attributes} - - try: - response = await self._parent.requester.post(*self._parent.get_own_path(), "configs", data=payload) - except RequesterError: - # config isn't saved, no data update is in play, - # returning "pre-saved" parsed values - self._parse_json_fields_inplace_safe(config_to_save) - - raise - else: - new_config = ConfigData.from_v2_response(data_in_v2_format=response.as_dict()) - self._initial_config = self._parse_json_fields_inplace_safe(new_config) - self.reset() - - return self - # Public For Internal Use Only @property @@ -369,7 +349,7 @@ async def _retrieve_current_config(self: Self) -> ConfigData: return self._parse_json_fields_inplace_safe(config_data) -class _RefreshableConfig[T: _ConfigWrapperCreator](_GeneralConfig[T]): +class _SaveableConfig[T: _ConfigWrapperCreator](_GeneralConfig[T]): async def refresh(self: Self, strategy: ConfigRefreshStrategy = apply_local_changes) -> Self: remote_config = await retrieve_current_config( parent=self._parent, get_schema=partial(retrieve_schema, parent=self._parent) @@ -386,6 +366,26 @@ async def refresh(self: Self, strategy: ConfigRefreshStrategy = apply_local_chan return self + async def save(self: Self, description: str = "") -> Self: + config_to_save = self._current_config.config + self._serialize_json_fields_inplace_safe(config_to_save) + payload = {"description": description, "config": config_to_save.values, "adcmMeta": config_to_save.attributes} + + try: + response = await self._parent.requester.post(*self._parent.get_own_path(), "configs", data=payload) + except RequesterError: + # config isn't saved, no data update is in play, + # returning "pre-saved" parsed values + self._parse_json_fields_inplace_safe(config_to_save) + + raise + else: + new_config = ConfigData.from_v2_response(data_in_v2_format=response.as_dict()) + self._initial_config = self._parse_json_fields_inplace_safe(new_config) + self.reset() + + return self + class ActionConfig(_GeneralConfig[ObjectConfigWrapper]): _wrapper_class = ObjectConfigWrapper @@ -403,8 +403,14 @@ def __getitem__[ExpectedType: ConfigEntry]( ) -> ConfigEntry: return self._current_config[item] + def _to_payload(self: Self) -> dict: + # don't want complexity of regular config with rollbacks on failure + config_to_save = deepcopy(self._current_config.config) + self._serialize_json_fields_inplace_safe(config_to_save) + return {"config": config_to_save.values, "adcmMeta": config_to_save.attributes} + -class ObjectConfig(_RefreshableConfig[ObjectConfigWrapper]): +class ObjectConfig(_SaveableConfig[ObjectConfigWrapper]): _wrapper_class = ObjectConfigWrapper # todo fix typing copy-paste @@ -422,7 +428,7 @@ def __getitem__[ExpectedType: ConfigEntry]( return self._current_config[item] -class HostGroupConfig(_RefreshableConfig[HostGroupConfigWrapper]): +class HostGroupConfig(_SaveableConfig[HostGroupConfigWrapper]): _wrapper_class = HostGroupConfigWrapper @overload diff --git a/adcm_aio_client/core/errors.py b/adcm_aio_client/core/errors.py index f9fd4aa..da598ae 100644 --- a/adcm_aio_client/core/errors.py +++ b/adcm_aio_client/core/errors.py @@ -126,10 +126,13 @@ class ConfigComparisonError(ConfigError): ... class ConfigNoParameterError(ConfigError): ... -# Mapping +# Action -class NoMappingRulesForActionError(ADCMClientError): ... +class NoMappingInActionError(ADCMClientError): ... + + +class NoConfigInActionError(ADCMClientError): ... # Filtering diff --git a/adcm_aio_client/core/objects/_common.py b/adcm_aio_client/core/objects/_common.py index ff75c2f..0db1607 100644 --- a/adcm_aio_client/core/objects/_common.py +++ b/adcm_aio_client/core/objects/_common.py @@ -24,7 +24,9 @@ async def get_status(self: Self) -> str: class WithActions(WithProtectedRequester, AwareOfOwnPath): @cached_property def actions(self: Self) -> ActionsAccessor: - return ActionsAccessor(parent=self, path=(*self.get_own_path(), "actions"), requester=self._requester) + # `WithActions` can actually be InteractiveObject, but it isn't required + # based on usages, so for now it's just ignore + return ActionsAccessor(parent=self, path=(*self.get_own_path(), "actions"), requester=self._requester) # type: ignore[reportArgumentType] class WithConfig(ConfigOwner): From 16f78d2b4190af42f288359a7c529d70242c5619 Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Mon, 23 Dec 2024 15:00:03 +0500 Subject: [PATCH 04/16] ADCM-6210 More test on jobs + minor fixes --- adcm_aio_client/core/actions/_objects.py | 2 + adcm_aio_client/core/client.py | 6 ++- .../core/host_groups/action_group.py | 2 + adcm_aio_client/core/objects/cm.py | 2 +- adcm_aio_client/core/requesters.py | 11 ++++- tests/integration/conftest.py | 6 ++- tests/integration/setup_environment.py | 2 +- tests/integration/test_jobs.py | 45 ++++++++++++++----- 8 files changed, 58 insertions(+), 18 deletions(-) diff --git a/adcm_aio_client/core/actions/_objects.py b/adcm_aio_client/core/actions/_objects.py index 5ce7519..481feda 100644 --- a/adcm_aio_client/core/actions/_objects.py +++ b/adcm_aio_client/core/actions/_objects.py @@ -54,6 +54,8 @@ def display_name(self: Self) -> str: async def run(self: Self) -> Job: from adcm_aio_client.core.objects.cm import Job + await self._ensure_rich_data() + data = {"isVerbose": self._verbose, "shouldBlockObject": self._blocking} if self._has_mapping: mapping = await self.mapping diff --git a/adcm_aio_client/core/client.py b/adcm_aio_client/core/client.py index 5fbdf20..2992fc6 100644 --- a/adcm_aio_client/core/client.py +++ b/adcm_aio_client/core/client.py @@ -13,7 +13,7 @@ from functools import cached_property from typing import Self -from adcm_aio_client.core.objects.cm import ADCM, BundlesNode, ClustersNode, HostProvidersNode, HostsNode +from adcm_aio_client.core.objects.cm import ADCM, BundlesNode, ClustersNode, HostProvidersNode, HostsNode, JobsNode from adcm_aio_client.core.requesters import BundleRetrieverInterface, Requester MIN_ADCM_VERSION = "2.5.0" @@ -48,3 +48,7 @@ def bundles(self: Self) -> BundlesNode: return BundlesNode( path=("bundles",), requester=self._requester, retriever=self._retrieve_bundle_from_remote_url ) + + @cached_property + def jobs(self: Self) -> JobsNode: + return JobsNode(path=("tasks",), requester=self._requester) diff --git a/adcm_aio_client/core/host_groups/action_group.py b/adcm_aio_client/core/host_groups/action_group.py index 63f4897..d6e6f17 100644 --- a/adcm_aio_client/core/host_groups/action_group.py +++ b/adcm_aio_client/core/host_groups/action_group.py @@ -1,6 +1,7 @@ from functools import cached_property from typing import TYPE_CHECKING, Self, Union +from adcm_aio_client.core.filters import FilterByName, Filtering from adcm_aio_client.core.host_groups._common import HostGroupNode, HostsInHostGroupNode from adcm_aio_client.core.objects._base import InteractiveChildObject from adcm_aio_client.core.objects._common import Deletable, WithActions @@ -28,6 +29,7 @@ def hosts(self: Self) -> "HostsInActionHostGroupNode": class ActionHostGroupNode(HostGroupNode[Union["Cluster", "Service", "Component"], ActionHostGroup]): class_type = ActionHostGroup + filtering = Filtering(FilterByName) class HostsInActionHostGroupNode(HostsInHostGroupNode): diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index bbb5f1a..594a74a 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -614,7 +614,7 @@ def _prepare_filter_by_object(self: Self, object_: InteractiveObject | None) -> elif isinstance(object_, HostProvider): object_type = "provider" elif isinstance(object_, ActionHostGroup): - object_type = "action-host-group" + object_type = "action_host_group" else: message = f"Failed to build filter: {object_.__class__.__name__} " "can't be an owner of Job" raise InvalidFilterError(message) diff --git a/adcm_aio_client/core/requesters.py b/adcm_aio_client/core/requesters.py index 91e82c6..f02127c 100644 --- a/adcm_aio_client/core/requesters.py +++ b/adcm_aio_client/core/requesters.py @@ -110,10 +110,13 @@ def retry_request(request_func: RequestFunc) -> RequestFunc: @wraps(request_func) async def wrapper(self: "DefaultRequester", *args: Params.args, **kwargs: Params.kwargs) -> HTTPXRequesterResponse: retries = self._retries + last_error = None + for attempt in range(retries.attempts): try: response = await request_func(self, *args, **kwargs) - except (UnauthorizedError, httpx.NetworkError, httpx.TransportError): + except (UnauthorizedError, httpx.NetworkError, httpx.TransportError) as e: + last_error = e if attempt >= retries.attempts - 1: continue @@ -125,7 +128,11 @@ async def wrapper(self: "DefaultRequester", *args: Params.args, **kwargs: Params break else: message = f"Request failed in {retries.interval} attempts" - raise RetryRequestError(message) + if last_error is None: + raise RetryRequestError(message) + + message = f"{message}. Last error: {last_error}" + raise RetryRequestError(message) from last_error return response diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index af97567..4b1387a 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -49,10 +49,12 @@ def adcm(network: Network, postgres: ADCMPostgresContainer) -> Generator[ADCMCon @pytest_asyncio.fixture(scope="function") -async def adcm_client(adcm: ADCMContainer) -> AsyncGenerator[ADCMClient, None]: +async def adcm_client(request: pytest.FixtureRequest, adcm: ADCMContainer) -> AsyncGenerator[ADCMClient, None]: credentials = Credentials(username="admin", password="admin") # noqa: S106 url = adcm.url - async with ADCMSession(url=url, credentials=credentials, timeout=10, retry_interval=1, retry_attempts=1) as client: + extra_kwargs = request.param or {} + kwargs: dict = {"timeout": 10, "retry_interval": 1, "retry_attempts": 1} | extra_kwargs + async with ADCMSession(url=url, credentials=credentials, **kwargs) as client: yield client diff --git a/tests/integration/setup_environment.py b/tests/integration/setup_environment.py index 1b7d519..6dbbd35 100644 --- a/tests/integration/setup_environment.py +++ b/tests/integration/setup_environment.py @@ -9,7 +9,7 @@ from testcontainers.postgres import DbContainer, PostgresContainer postgres_image_name = "postgres:latest" -adcm_image_name = "hub.adsw.io/adcm/adcm:develop" +adcm_image_name = "hub.adsw.io/adcm/adcm:ADCM-6137" adcm_container_name = "test_adcm" postgres_name = "test_pg_db" diff --git a/tests/integration/test_jobs.py b/tests/integration/test_jobs.py index 3eb1e28..4e0fefc 100644 --- a/tests/integration/test_jobs.py +++ b/tests/integration/test_jobs.py @@ -9,16 +9,31 @@ from adcm_aio_client.core.filters import Filter, FilterValue from adcm_aio_client.core.objects._common import WithActions from adcm_aio_client.core.objects.cm import Bundle, Cluster, Component, Job +from adcm_aio_client.core.types import WithID pytestmark = [pytest.mark.asyncio] +async def is_running(job: Job) -> bool: + return await job.get_status() == "running" + + async def run_non_blocking(target: WithActions, **filters: FilterValue) -> Job: action = await target.actions.get(**filters) action.blocking = False return await action.run() +async def check_job_object(job: Job, object_: WithID) -> None: + expected_type = object_.__class__ + expected_id = object_.id + + actual_object = await job.object + + assert isinstance(actual_object, expected_type) + assert actual_object.id == expected_id + + @pytest_asyncio.fixture() async def prepare_environment( adcm_client: ADCMClient, @@ -68,21 +83,14 @@ async def prepare_environment( @pytest.mark.usefixtures("prepare_environment") +@pytest.mark.parametrize("adcm_client", [{"timeout": 60}], ids=["t60"], indirect=True) async def test_jobs_api(adcm_client: ADCMClient) -> None: await _test_basic_api(adcm_client) await _test_job_object(adcm_client) await _test_collection_fitlering(adcm_client) -async def is_running(job: Job) -> bool: - return await job.get_status() == "running" - - async def _test_basic_api(adcm_client: ADCMClient) -> None: - # fields: id, name, display_name, start_time, finish_time - # properties: object, action - # methods: get_status, wait, terminate - # refresh (with wait) cluster, *_ = await adcm_client.clusters.list(query={"limit": 1, "offset": 3}) service = await cluster.services.get(name__contains="action") component = await service.components.get(display_name__icontains="wESo") @@ -95,7 +103,7 @@ async def _test_basic_api(adcm_client: ADCMClient) -> None: assert job.finish_time is None assert (await job.action).id == action.id - await job.wait(exit_condition=is_running, timeout=5, poll_interval=1) + await job.wait(exit_condition=is_running, timeout=20, poll_interval=1) assert job.start_time is None await job.refresh() assert isinstance(job.start_time, datetime) @@ -106,7 +114,7 @@ async def _test_basic_api(adcm_client: ADCMClient) -> None: assert target.id == component.id assert target.service.id == component.service.id - await job.wait(timeout=10, poll_interval=1) + await job.wait(timeout=30, poll_interval=3) assert await job.get_status() == "success" assert job.finish_time is None @@ -117,7 +125,22 @@ async def _test_basic_api(adcm_client: ADCMClient) -> None: async def _test_job_object(adcm_client: ADCMClient) -> None: # filter by object # detect object from Job - ... + cluster, *_ = await adcm_client.clusters.list(query={"limit": 1, "offset": 4}) + service = await cluster.services.get() + component, *_ = await service.components.all() + hostprovider, *_ = await adcm_client.hostproviders.list(query={"limit": 1, "offset": 2}) + host, *_ = await adcm_client.hosts.list(query={"limit": 1, "offset": 4}) + + host_group_1 = await service.action_host_groups.get() + host_group_2 = await component.action_host_groups.get() + + all_targets = (cluster, service, component, hostprovider, host, host_group_1, host_group_2) + + for target in all_targets: + jobs = await adcm_client.jobs.filter(object=target) + assert len(jobs) == 1, f"Amount of jobs is incorrect for {target}: {len(jobs)}. Expected 1" + job = jobs[0] + await check_job_object(job=job, object_=target) # type: ignore async def _test_collection_fitlering(adcm_client: ADCMClient) -> None: From 255781307e61d0cba4b5991313df5fc06bbd6305 Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Mon, 23 Dec 2024 15:45:00 +0500 Subject: [PATCH 05/16] ADCM-6210 Fix tests --- tests/integration/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 4b1387a..ffb973a 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -52,7 +52,7 @@ def adcm(network: Network, postgres: ADCMPostgresContainer) -> Generator[ADCMCon async def adcm_client(request: pytest.FixtureRequest, adcm: ADCMContainer) -> AsyncGenerator[ADCMClient, None]: credentials = Credentials(username="admin", password="admin") # noqa: S106 url = adcm.url - extra_kwargs = request.param or {} + extra_kwargs = getattr(request, "param", {}) kwargs: dict = {"timeout": 10, "retry_interval": 1, "retry_attempts": 1} | extra_kwargs async with ADCMSession(url=url, credentials=credentials, **kwargs) as client: yield client From fc43e91e4cf1215a68b6fd1fea3a03ed6f613819 Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Mon, 23 Dec 2024 16:46:09 +0500 Subject: [PATCH 06/16] ADCM-6210 Add filter by hostprovider for hosts node --- adcm_aio_client/core/objects/cm.py | 2 +- tests/integration/test_jobs.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 594a74a..3881083 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -410,7 +410,7 @@ async def hostprovider(self: Self) -> HostProvider: class HostsAccessor(PaginatedAccessor[Host]): class_type = Host - filtering = Filtering(FilterByName, FilterByStatus) + filtering = Filtering(FilterByName, FilterByStatus, FilterBy("hostprovider", COMMON_OPERATIONS, HostProvider)) class HostsNode(HostsAccessor): diff --git a/tests/integration/test_jobs.py b/tests/integration/test_jobs.py index 4e0fefc..b687be0 100644 --- a/tests/integration/test_jobs.py +++ b/tests/integration/test_jobs.py @@ -123,8 +123,6 @@ async def _test_basic_api(adcm_client: ADCMClient) -> None: async def _test_job_object(adcm_client: ADCMClient) -> None: - # filter by object - # detect object from Job cluster, *_ = await adcm_client.clusters.list(query={"limit": 1, "offset": 4}) service = await cluster.services.get() component, *_ = await service.components.all() From 70756386246de231b206bea2fdf15618fe756a6b Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Mon, 23 Dec 2024 18:43:38 +0500 Subject: [PATCH 07/16] ADCM-6210 More test --- adcm_aio_client/core/objects/cm.py | 4 ++ tests/integration/test_jobs.py | 89 ++++++++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 3881083..4c40a63 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -471,6 +471,10 @@ class Job(WithStatus, RootInteractiveObject): def name(self: Self) -> str: return str(self._data["name"]) + @property + def display_name(self: Self) -> str: + return str(self._data["displayName"]) + @cached_property def start_time(self: Self) -> datetime | None: time = self._data["startTime"] diff --git a/tests/integration/test_jobs.py b/tests/integration/test_jobs.py index b687be0..9fa374d 100644 --- a/tests/integration/test_jobs.py +++ b/tests/integration/test_jobs.py @@ -1,5 +1,6 @@ from datetime import datetime from itertools import chain +from operator import attrgetter import asyncio import pytest @@ -7,6 +8,7 @@ from adcm_aio_client.core.client import ADCMClient from adcm_aio_client.core.filters import Filter, FilterValue +from adcm_aio_client.core.host_groups.action_group import ActionHostGroup from adcm_aio_client.core.objects._common import WithActions from adcm_aio_client.core.objects.cm import Bundle, Cluster, Component, Job from adcm_aio_client.core.types import WithID @@ -91,7 +93,7 @@ async def test_jobs_api(adcm_client: ADCMClient) -> None: async def _test_basic_api(adcm_client: ADCMClient) -> None: - cluster, *_ = await adcm_client.clusters.list(query={"limit": 1, "offset": 3}) + cluster = await adcm_client.clusters.get(name__eq="wow-4") service = await cluster.services.get(name__contains="action") component = await service.components.get(display_name__icontains="wESo") @@ -103,7 +105,7 @@ async def _test_basic_api(adcm_client: ADCMClient) -> None: assert job.finish_time is None assert (await job.action).id == action.id - await job.wait(exit_condition=is_running, timeout=20, poll_interval=1) + await job.wait(exit_condition=is_running, timeout=30, poll_interval=1) assert job.start_time is None await job.refresh() assert isinstance(job.start_time, datetime) @@ -125,7 +127,7 @@ async def _test_basic_api(adcm_client: ADCMClient) -> None: async def _test_job_object(adcm_client: ADCMClient) -> None: cluster, *_ = await adcm_client.clusters.list(query={"limit": 1, "offset": 4}) service = await cluster.services.get() - component, *_ = await service.components.all() + component = await service.components.get(name__eq="c2") hostprovider, *_ = await adcm_client.hostproviders.list(query={"limit": 1, "offset": 2}) host, *_ = await adcm_client.hosts.list(query={"limit": 1, "offset": 4}) @@ -144,4 +146,83 @@ async def _test_job_object(adcm_client: ADCMClient) -> None: async def _test_collection_fitlering(adcm_client: ADCMClient) -> None: # filters: status, name, display_name, action # special filter: object - ... + failed_jobs = 20 + + jobs = await adcm_client.jobs.list() + assert len(jobs) == 50 + + jobs = await adcm_client.jobs.all() + total_jobs = len(jobs) + assert total_jobs > 50 + + print("===========jobs================") + for job in jobs: + print(job.name, job.display_name, await job.get_status()) + + cases = ( + # status + ("status__eq", "failed", failed_jobs), + ("status__ieq", "faiLed", failed_jobs), + ("status__ne", "success", failed_jobs), + ("status__ine", "succEss", failed_jobs), + ("status__in", ("failed", "success"), total_jobs), + ("status__iin", ("faIled", "sUcceSs"), total_jobs), + ("status__exclude", ("failed", "success"), 0), + ("status__iexclude", ("succesS",), failed_jobs), + # name + ("name__eq", "fail", failed_jobs), + ("name__ieq", "FaIl", failed_jobs), + ("name__ne", "fail", total_jobs - failed_jobs), + ("name__ine", "FaIl", total_jobs - failed_jobs), + ("name__in", ("success", "success_task"), total_jobs - failed_jobs), + ("name__iin", ("sUccEss", "success_Task"), total_jobs - failed_jobs), + ("name__exclude", ("success",), 4), + ("name__iexclude", ("success",), 4), + ("name__contains", "il", failed_jobs), + ("name__icontains", "I", total_jobs - 1), + # display_name + ("display_name__eq", "no Way", failed_jobs), + ("display_name__ieq", "No way", failed_jobs), + ("display_name__ne", "no Way", total_jobs - failed_jobs), + ("display_name__ine", "No way", total_jobs - failed_jobs), + ("display_name__in", ("I will survive", "Lots Of me"), total_jobs - failed_jobs), + ("display_name__iin", ("i will survive", "lots of me"), total_jobs - failed_jobs), + ("display_name__exclude", ("I will survive",), 4), + ("display_name__iexclude", ("i will survive",), 4), + ("display_name__contains", "W", failed_jobs), + ("display_name__icontains", "W", total_jobs - 1), + ) + + for inline_filter, value, expected_amount in cases: + filter_ = {inline_filter: value} + result = await adcm_client.jobs.filter(**filter_) # type: ignore + actual_amount = len(result) + assert ( + actual_amount == expected_amount + ), f"Incorrect amount for {filter_=}\nExpected: {expected_amount}\nActual: {actual_amount}" + unique_entries = set(map(attrgetter("id"), result)) + assert len(unique_entries) == expected_amount + + cluster = await adcm_client.clusters.get(name__eq="wow-4") + service = await cluster.services.get() + service_ahg = await service.action_host_groups.get() + + fail_action = await service.actions.get(name__eq="fail") + success_action = await service.actions.get(name__eq="success") + + jobs = [job async for job in adcm_client.jobs.iter(action__eq=fail_action)] + assert len(jobs) == 5 + objects = [] + for job in jobs: + objects.append(await job.object) + assert all(isinstance(o, ActionHostGroup) for o in objects) + assert any(o.id == service_ahg.id for o in objects) + + job = await adcm_client.jobs.get_or_none(action__in=(fail_action, success_action), status__eq="notexist") + assert job is None + + jobs = await adcm_client.jobs.filter(action__ne=success_action) + assert len(jobs) == failed_jobs + 1 + + jobs = await adcm_client.jobs.filter(action__exclude=(success_action,)) + assert len(jobs) == failed_jobs + 1 From b26b3e0e0a2f2858d29155cd0f37888499165bfb Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Mon, 23 Dec 2024 19:44:55 +0500 Subject: [PATCH 08/16] ADCM-6210 add `actions.yaml` to hostprovider bundle --- .../bundles/simple_hostprovider/actions.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/integration/bundles/simple_hostprovider/actions.yaml diff --git a/tests/integration/bundles/simple_hostprovider/actions.yaml b/tests/integration/bundles/simple_hostprovider/actions.yaml new file mode 100644 index 0000000..95d47d2 --- /dev/null +++ b/tests/integration/bundles/simple_hostprovider/actions.yaml @@ -0,0 +1,16 @@ +- name: letsgo + hosts: localhost + connection: local + gather_facts: no + + tasks: + - name: Success + debug: + msg: "successful step" + tags: [ok] + + - name: Fail + fail: + msg: "failed step" + tags: [fail] + From d394ab4a8cd67c65c15e7748ea9a70b467a0c5b0 Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Tue, 24 Dec 2024 12:11:42 +0500 Subject: [PATCH 09/16] ADCM-6210 Test on jobs finished --- adcm_aio_client/core/objects/cm.py | 2 +- tests/integration/setup_environment.py | 2 +- tests/integration/test_jobs.py | 21 ++++++++------------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 4c40a63..92b6ad1 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -589,7 +589,7 @@ async def get(self: Self, *, object: InteractiveObject | None = None, **filters: async def get_or_none(self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue) -> Job | None: # noqa: A002 object_filter = self._prepare_filter_by_object(object) all_filters = filters | object_filter - return await super().get(**all_filters) + return await super().get_or_none(**all_filters) async def filter(self: Self, *, object: InteractiveObject | None = None, **filters: FilterValue) -> list[Job]: # noqa: A002 object_filter = self._prepare_filter_by_object(object) diff --git a/tests/integration/setup_environment.py b/tests/integration/setup_environment.py index 6dbbd35..1b7d519 100644 --- a/tests/integration/setup_environment.py +++ b/tests/integration/setup_environment.py @@ -9,7 +9,7 @@ from testcontainers.postgres import DbContainer, PostgresContainer postgres_image_name = "postgres:latest" -adcm_image_name = "hub.adsw.io/adcm/adcm:ADCM-6137" +adcm_image_name = "hub.adsw.io/adcm/adcm:develop" adcm_container_name = "test_adcm" postgres_name = "test_pg_db" diff --git a/tests/integration/test_jobs.py b/tests/integration/test_jobs.py index 9fa374d..e93fbd1 100644 --- a/tests/integration/test_jobs.py +++ b/tests/integration/test_jobs.py @@ -144,9 +144,8 @@ async def _test_job_object(adcm_client: ADCMClient) -> None: async def _test_collection_fitlering(adcm_client: ADCMClient) -> None: - # filters: status, name, display_name, action - # special filter: object failed_jobs = 20 + services_amount = 5 jobs = await adcm_client.jobs.list() assert len(jobs) == 50 @@ -155,10 +154,6 @@ async def _test_collection_fitlering(adcm_client: ADCMClient) -> None: total_jobs = len(jobs) assert total_jobs > 50 - print("===========jobs================") - for job in jobs: - print(job.name, job.display_name, await job.get_status()) - cases = ( # status ("status__eq", "failed", failed_jobs), @@ -176,10 +171,10 @@ async def _test_collection_fitlering(adcm_client: ADCMClient) -> None: ("name__ine", "FaIl", total_jobs - failed_jobs), ("name__in", ("success", "success_task"), total_jobs - failed_jobs), ("name__iin", ("sUccEss", "success_Task"), total_jobs - failed_jobs), - ("name__exclude", ("success",), 4), - ("name__iexclude", ("success",), 4), + ("name__exclude", ("success",), failed_jobs + 1), + ("name__iexclude", ("success",), failed_jobs + 1), ("name__contains", "il", failed_jobs), - ("name__icontains", "I", total_jobs - 1), + ("name__icontains", "I", failed_jobs), # display_name ("display_name__eq", "no Way", failed_jobs), ("display_name__ieq", "No way", failed_jobs), @@ -187,8 +182,8 @@ async def _test_collection_fitlering(adcm_client: ADCMClient) -> None: ("display_name__ine", "No way", total_jobs - failed_jobs), ("display_name__in", ("I will survive", "Lots Of me"), total_jobs - failed_jobs), ("display_name__iin", ("i will survive", "lots of me"), total_jobs - failed_jobs), - ("display_name__exclude", ("I will survive",), 4), - ("display_name__iexclude", ("i will survive",), 4), + ("display_name__exclude", ("I will survive",), failed_jobs + 1), + ("display_name__iexclude", ("i will survive",), failed_jobs + 1), ("display_name__contains", "W", failed_jobs), ("display_name__icontains", "W", total_jobs - 1), ) @@ -222,7 +217,7 @@ async def _test_collection_fitlering(adcm_client: ADCMClient) -> None: assert job is None jobs = await adcm_client.jobs.filter(action__ne=success_action) - assert len(jobs) == failed_jobs + 1 + assert len(jobs) == total_jobs - services_amount jobs = await adcm_client.jobs.filter(action__exclude=(success_action,)) - assert len(jobs) == failed_jobs + 1 + assert len(jobs) == total_jobs - services_amount From 9806cf32895fc38dce5b30580a7ea518d504ab18 Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Tue, 24 Dec 2024 16:05:12 +0500 Subject: [PATCH 10/16] ADCM-6210 Add timeout lib --- poetry.lock | 16 +++++++++++++++- pyproject.toml | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 73fffa4..82be485 100644 --- a/poetry.lock +++ b/poetry.lock @@ -385,6 +385,20 @@ pytest = ">=8.2,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "pytest-timeout" +version = "2.3.1" +description = "pytest plugin to abort hanging tests" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, + {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + [[package]] name = "pywin32" version = "308" @@ -701,4 +715,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "87a74f8686fa2e421979ffe2ffd12e46a54176e4852f184fd5782f252d117c1b" +content-hash = "f7bae9e0c1c116fe81eca25b419ce706a5b74821d1910f35632270f51a75727e" diff --git a/pyproject.toml b/pyproject.toml index 8492d35..294c2ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ pytest = "^8.3.3" pytest-asyncio = "^0.24.0" testcontainers = "^4.8.2" pyyaml = "^6.0.2" +pytest-timeout = "^2.3.1" [build-system] requires = ["poetry-core"] @@ -34,6 +35,7 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" +timeout = 300 [tool.ruff] line-length = 120 From 442c597420cc448606132f303a74b78a8d50b928 Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Tue, 24 Dec 2024 16:45:59 +0500 Subject: [PATCH 11/16] ADCM-6210 added test info --- pyproject.toml | 1 + tests/integration/test_jobs.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 294c2ee..2de0993 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" timeout = 300 +addopts = ["-vvvv"] [tool.ruff] line-length = 120 diff --git a/tests/integration/test_jobs.py b/tests/integration/test_jobs.py index e93fbd1..18ee29e 100644 --- a/tests/integration/test_jobs.py +++ b/tests/integration/test_jobs.py @@ -1,5 +1,6 @@ from datetime import datetime from itertools import chain +import logging from operator import attrgetter import asyncio @@ -72,6 +73,8 @@ async def prepare_environment( ) ) + logging.error("prepare for running actions") + object_jobs = asyncio.gather( *( run_non_blocking(object_, name__eq="success") @@ -81,12 +84,15 @@ async def prepare_environment( group_jobs = asyncio.gather(*(run_non_blocking(group, name__in=["fail"]) for group in host_groups)) + logging.error("actions launched") + return await object_jobs + await group_jobs @pytest.mark.usefixtures("prepare_environment") @pytest.mark.parametrize("adcm_client", [{"timeout": 60}], ids=["t60"], indirect=True) async def test_jobs_api(adcm_client: ADCMClient) -> None: + print("test started jobs api") await _test_basic_api(adcm_client) await _test_job_object(adcm_client) await _test_collection_fitlering(adcm_client) From c0caa2a5358e5c7f6a2381fd74466858e6d8a5ba Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Tue, 24 Dec 2024 17:32:02 +0500 Subject: [PATCH 12/16] Revert 1 commits 442c597 'ADCM-6210 added test info' --- pyproject.toml | 1 - tests/integration/test_jobs.py | 6 ------ 2 files changed, 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2de0993..294c2ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,6 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" timeout = 300 -addopts = ["-vvvv"] [tool.ruff] line-length = 120 diff --git a/tests/integration/test_jobs.py b/tests/integration/test_jobs.py index 18ee29e..e93fbd1 100644 --- a/tests/integration/test_jobs.py +++ b/tests/integration/test_jobs.py @@ -1,6 +1,5 @@ from datetime import datetime from itertools import chain -import logging from operator import attrgetter import asyncio @@ -73,8 +72,6 @@ async def prepare_environment( ) ) - logging.error("prepare for running actions") - object_jobs = asyncio.gather( *( run_non_blocking(object_, name__eq="success") @@ -84,15 +81,12 @@ async def prepare_environment( group_jobs = asyncio.gather(*(run_non_blocking(group, name__in=["fail"]) for group in host_groups)) - logging.error("actions launched") - return await object_jobs + await group_jobs @pytest.mark.usefixtures("prepare_environment") @pytest.mark.parametrize("adcm_client", [{"timeout": 60}], ids=["t60"], indirect=True) async def test_jobs_api(adcm_client: ADCMClient) -> None: - print("test started jobs api") await _test_basic_api(adcm_client) await _test_job_object(adcm_client) await _test_collection_fitlering(adcm_client) From 62d5fb5c79fbd47df281d5ea0205873ba5554809 Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Tue, 24 Dec 2024 18:45:06 +0500 Subject: [PATCH 13/16] ADCM-6210 Sequential action run --- tests/integration/test_jobs.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/integration/test_jobs.py b/tests/integration/test_jobs.py index e93fbd1..4d1f091 100644 --- a/tests/integration/test_jobs.py +++ b/tests/integration/test_jobs.py @@ -41,7 +41,7 @@ async def prepare_environment( adcm_client: ADCMClient, complex_cluster_bundle: Bundle, simple_hostprovider_bundle: Bundle, -) -> list[Job]: +) -> None: cluster_bundle = complex_cluster_bundle hostprovider_bundle = simple_hostprovider_bundle @@ -72,16 +72,11 @@ async def prepare_environment( ) ) - object_jobs = asyncio.gather( - *( - run_non_blocking(object_, name__eq="success") - for object_ in chain(clusters, services, components, hosts, hostproviders) - ) - ) + for object_ in chain(clusters, services, components, hosts, hostproviders): + await run_non_blocking(object_, name__eq="success") - group_jobs = asyncio.gather(*(run_non_blocking(group, name__in=["fail"]) for group in host_groups)) - - return await object_jobs + await group_jobs + for group in host_groups: + await run_non_blocking(group, name__in=["fail"]) @pytest.mark.usefixtures("prepare_environment") @@ -147,6 +142,9 @@ async def _test_collection_fitlering(adcm_client: ADCMClient) -> None: failed_jobs = 20 services_amount = 5 + for job in await adcm_client.jobs.all(): + await job.wait(timeout=60) + jobs = await adcm_client.jobs.list() assert len(jobs) == 50 From 2cde410e21b4945ec5fee9a7246d8fc21a343c1e Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Tue, 24 Dec 2024 19:06:30 +0500 Subject: [PATCH 14/16] ADCM-6210 Fixed conftest --- tests/integration/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 5108619..3aa53fc 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -71,6 +71,7 @@ async def simple_hostprovider_bundle(adcm_client: ADCMClient, tmp_path: Path) -> bundle_path = pack_bundle(from_dir=BUNDLES / "simple_hostprovider", to=tmp_path) return await adcm_client.bundles.create(source=bundle_path, accept_license=True) + @pytest_asyncio.fixture() async def httpx_client(adcm: ADCMContainer) -> AsyncGenerator[AsyncClient, None]: client = AsyncClient(base_url=urljoin(adcm.url, "api/v2/")) From 5b1067bffeb4b6e3fec68aa9b8d0f99e2e3a142c Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Tue, 24 Dec 2024 19:45:42 +0500 Subject: [PATCH 15/16] ADCM-6210 Skip jobs test temporarily --- tests/integration/test_jobs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/test_jobs.py b/tests/integration/test_jobs.py index 4d1f091..d60d1a2 100644 --- a/tests/integration/test_jobs.py +++ b/tests/integration/test_jobs.py @@ -79,6 +79,7 @@ async def prepare_environment( await run_non_blocking(group, name__in=["fail"]) +@pytest.mark.skip() @pytest.mark.usefixtures("prepare_environment") @pytest.mark.parametrize("adcm_client", [{"timeout": 60}], ids=["t60"], indirect=True) async def test_jobs_api(adcm_client: ADCMClient) -> None: From 9fac469a04e030687ffc82ce47c81931b988b5fd Mon Sep 17 00:00:00 2001 From: Egor Araslanov Date: Wed, 25 Dec 2024 09:41:43 +0500 Subject: [PATCH 16/16] Revert 1 commits 5b1067b 'ADCM-6210 Skip jobs test temporarily' --- tests/integration/test_jobs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/test_jobs.py b/tests/integration/test_jobs.py index d60d1a2..4d1f091 100644 --- a/tests/integration/test_jobs.py +++ b/tests/integration/test_jobs.py @@ -79,7 +79,6 @@ async def prepare_environment( await run_non_blocking(group, name__in=["fail"]) -@pytest.mark.skip() @pytest.mark.usefixtures("prepare_environment") @pytest.mark.parametrize("adcm_client", [{"timeout": 60}], ids=["t60"], indirect=True) async def test_jobs_api(adcm_client: ADCMClient) -> None: