Skip to content

Commit

Permalink
ADCM-6210 Test on Jobs and related fixes (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sealwing authored Dec 25, 2024
1 parent d0fa697 commit ae30c15
Show file tree
Hide file tree
Showing 19 changed files with 647 additions and 103 deletions.
122 changes: 95 additions & 27 deletions adcm_aio_client/core/actions/_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

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
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):
Expand All @@ -21,6 +23,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:
Expand All @@ -30,40 +51,94 @@ 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

@async_cached_property
async def _mapping_rule(self: Self) -> list[dict] | None:
return (await self._rich_data)["hostComponentMapRules"]
await self._ensure_rich_data()

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(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
entries = mapping.all()

return ActionMapping(owner=self._parent, cluster=cluster, entries=entries)

def set_verbose(self: Self) -> Self:
self._verbose = True
return self
@async_cached_property
async def config(self: Self) -> ActionConfig:
await self._ensure_rich_data()

@async_cached_property # TODO: Config class
async def config(self: Self) -> ...:
return (await self._rich_data)["configuration"]
if not self._has_config:
message = f"Action {self.display_name} doesn't allow config changes"
raise NoConfigInActionError(message)

@async_cached_property
async def _rich_data(self: Self) -> dict:
return (await self._requester.get(*self.get_own_path())).as_dict()
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

class ActionsAccessor(NonPaginatedChildAccessor):
@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]):
class_type = Action
filtering = Filtering(FilterByName, FilterByDisplayName)

Expand All @@ -77,13 +152,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
Expand Down
6 changes: 5 additions & 1 deletion adcm_aio_client/core/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
52 changes: 29 additions & 23 deletions adcm_aio_client/core/config/_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions adcm_aio_client/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,13 @@ class ConfigComparisonError(ConfigError): ...
class ConfigNoParameterError(ConfigError): ...


# Mapping
# Action


class NoMappingRulesForActionError(ADCMClientError): ...
class NoMappingInActionError(ADCMClientError): ...


class NoConfigInActionError(ADCMClientError): ...


# Filtering
Expand Down
5 changes: 3 additions & 2 deletions adcm_aio_client/core/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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)
3 changes: 1 addition & 2 deletions adcm_aio_client/core/host_groups/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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())

Expand Down
9 changes: 2 additions & 7 deletions adcm_aio_client/core/host_groups/action_group.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
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.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
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
Expand All @@ -27,10 +26,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
Expand Down
8 changes: 6 additions & 2 deletions adcm_aio_client/core/objects/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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__(
Expand Down
5 changes: 3 additions & 2 deletions adcm_aio_client/core/objects/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ 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]


# todo whole section lacking implementation (and maybe code move is required)
class WithConfig(ConfigOwner):
@cached_property
async def config(self: Self) -> ObjectConfig:
Expand Down
Loading

0 comments on commit ae30c15

Please sign in to comment.