Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADCM-6210 Test on Jobs and related fixes #52

Merged
merged 18 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
DanBalalan marked this conversation as resolved.
Show resolved Hide resolved
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