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-6240: [With ADCM API error inspection] Redesign the error detection mechanism and make them more user-friendly #58

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
16 changes: 14 additions & 2 deletions adcm_aio_client/core/actions/_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@

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.errors import (
ConflictError,
HostNotInClusterError,
NoConfigInActionError,
NoMappingInActionError,
ObjectBlockedError,
)
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
Expand Down Expand Up @@ -64,7 +70,13 @@ async def run(self: Self) -> Job:
config = await self.config
data |= {"configuration": config._to_payload()}

response = await self._requester.post(*self.get_own_path(), "run", data=data)
try:
response = await self._requester.post(*self.get_own_path(), "run", data=data)
except ConflictError as e:
if "has issue" in str(e):
raise ObjectBlockedError(*e.args) from None
raise

return Job(requester=self._requester, data=response.as_dict())

@async_cached_property
Expand Down
22 changes: 20 additions & 2 deletions adcm_aio_client/core/config/_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
LevelNames,
LocalConfigs,
)
from adcm_aio_client.core.errors import ConfigComparisonError, ConfigNoParameterError, RequesterError
from adcm_aio_client.core.errors import (
BadRequestError,
ConfigComparisonError,
ConfigNoParameterError,
InvalidConfigError,
RequesterError,
)
from adcm_aio_client.core.types import AwareOfOwnPath, WithRequesterProperty


Expand Down Expand Up @@ -373,11 +379,23 @@ async def save(self: Self, description: str = "") -> Self:

try:
response = await self._parent.requester.post(*self._parent.get_own_path(), "configs", data=payload)
except RequesterError:
except RequesterError as e:
# config isn't saved, no data update is in play,
# returning "pre-saved" parsed values
self._parse_json_fields_inplace_safe(config_to_save)
if isinstance(e, BadRequestError):
raise InvalidConfigError(*e.args) from None
raise

try:
response = await self._parent.requester.post(*self._parent.get_own_path(), "configs", data=payload)
except RequesterError as e:
# config isn't saved, no data update is in play,
# returning "pre-saved" parsed values
self._parse_json_fields_inplace_safe(config_to_save)

if isinstance(e, BadRequestError):
raise InvalidConfigError(*e.args) from None
raise
else:
new_config = ConfigData.from_v2_response(data_in_v2_format=response.as_dict())
Expand Down
51 changes: 39 additions & 12 deletions adcm_aio_client/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class ADCMClientError(Exception):
pass


class WaitTimeoutError(ADCMClientError):
pass


# Session


Expand All @@ -25,10 +29,6 @@ class ClientInitError(ADCMClientError):
# Version


class VersionRetrievalError(ADCMClientError):
pass


class NotSupportedVersionError(ADCMClientError):
pass

Expand All @@ -44,7 +44,7 @@ class NoCredentialsError(RequesterError):
pass


class WrongCredentialsError(RequesterError):
class AuthenticationError(RequesterError):
pass


Expand All @@ -64,31 +64,31 @@ class ResponseDataConversionError(RequesterError):
pass


class ResponseError(RequesterError):
class UnknownError(RequesterError):
pass


class BadRequestError(ResponseError):
class BadRequestError(UnknownError):
pass


class UnauthorizedError(ResponseError):
class UnauthorizedError(UnknownError):
pass


class ForbiddenError(ResponseError):
class PermissionDeniedError(UnknownError):
pass


class NotFoundError(ResponseError):
class NotFoundError(UnknownError):
pass


class ConflictError(ResponseError):
class ConflictError(UnknownError):
pass


class ServerError(ResponseError):
class ServerError(UnknownError):
pass


Expand All @@ -107,6 +107,10 @@ class ObjectDoesNotExistError(AccessorError):
pass


class ObjectAlreadyExistsError(AccessorError): # TODO: add tests
pass


class OperationError(AccessorError):
pass

Expand Down Expand Up @@ -142,3 +146,26 @@ class FilterError(ADCMClientError): ...


class InvalidFilterError(FilterError): ...


# Operation-related


class HostConflictError(ADCMClientError):
pass


class ObjectBlockedError(ADCMClientError):
pass


class InvalidMappingError(ADCMClientError):
pass


class InvalidConfigError(ADCMClientError):
pass


class ObjectUpdateError(ADCMClientError):
pass
26 changes: 18 additions & 8 deletions adcm_aio_client/core/host_groups/_common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from functools import partial
from typing import TYPE_CHECKING, Any, Iterable, Self, Union

from adcm_aio_client.core.errors import ConflictError, HostConflictError, ObjectAlreadyExistsError
from adcm_aio_client.core.filters import Filter
from adcm_aio_client.core.objects._accessors import (
DefaultQueryParams as AccessorFilter,
Expand Down Expand Up @@ -54,13 +55,17 @@ async def set(self: Self, host: Union["Host", Iterable["Host"], Filter]) -> None

async def _add_hosts_to_group(self: Self, ids: Iterable[HostID]) -> None:
add_by_id = partial(self._requester.post, *self._path)
add_coros = map(add_by_id, ({"hostId": id_} for id_ in ids))
error = await safe_gather(
coros=add_coros,
msg=f"Some hosts can't be added to {self.group_type} host group",
)
if error is not None:
raise error
try:
if error := await safe_gather(
coros=(add_by_id(data={"hostId": id_}) for id_ in ids),
msg=f"Some hosts can't be added to {self.group_type} host group",
):
raise error
except* ConflictError as conflict_err_group:
host_conflict_msgs = {"already a member of this group", "already is a member of another group"}
if target_gr := conflict_err_group.subgroup(lambda e: any(msg in str(e) for msg in host_conflict_msgs)):
raise HostConflictError(*target_gr.exceptions[0].args) from None
raise

async def _remove_hosts_from_group(self: Self, ids: Iterable[HostID]) -> None:
delete_by_id = partial(self._requester.delete, *self._path)
Expand Down Expand Up @@ -99,7 +104,12 @@ class HostGroupNode[
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
) -> Child:
response = await self._requester.post(*self._path, data={"name": name, "description": description})
try:
response = await self._requester.post(*self._path, data={"name": name, "description": description})
except ConflictError as e:
if "already exists" in str(e):
raise ObjectAlreadyExistsError(*e.args) from None
raise
host_group = self.class_type(parent=self._parent, data=response.as_dict())

if not hosts:
Expand Down
20 changes: 19 additions & 1 deletion adcm_aio_client/core/mapping/_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Iterable, Self
import asyncio

from adcm_aio_client.core.errors import BadRequestError, ConflictError, InvalidMappingError
from adcm_aio_client.core.filters import Filter, FilterByDisplayName, FilterByName, FilterByStatus, Filtering
from adcm_aio_client.core.mapping.refresh import apply_local_changes, apply_remote_changes
from adcm_aio_client.core.mapping.types import LocalMappings, MappingEntry, MappingPair, MappingRefreshStrategy
Expand Down Expand Up @@ -149,7 +150,24 @@ async def for_cluster(cls: type[Self], owner: Cluster) -> Self:
async def save(self: Self) -> Self:
data = self._to_payload()

await self._requester.post(*self._cluster.get_own_path(), "mapping", data=data)
try:
await self._requester.post(*self._cluster.get_own_path(), "mapping", data=data)
except ConflictError as e:
# TODO: may be incomplete. Add tests for errors
conflict_msgs = {
"has unsatisfied constraint",
"No required service",
"hosts in maintenance mode",
"COMPONENT_CONSTRAINT_ERROR",
}
if any(msg in str(e) for msg in conflict_msgs):
raise InvalidMappingError(*e.args) from None
raise
except BadRequestError as e:
bad_request_msgs = {"Mapping entries duplicates found"}
if any((msg in str(e)) for msg in bad_request_msgs):
raise InvalidMappingError(*e.args) from None
raise

self._initial = copy(self._current)

Expand Down
85 changes: 61 additions & 24 deletions adcm_aio_client/core/objects/cm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@
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 InvalidFilterError, NotFoundError
from adcm_aio_client.core.errors import (
ConflictError,
HostConflictError,
InvalidFilterError,
NotFoundError,
ObjectAlreadyExistsError,
ObjectUpdateError,
UnknownError,
WaitTimeoutError,
)
from adcm_aio_client.core.filters import (
ALL_OPERATIONS,
COMMON_OPERATIONS,
Expand Down Expand Up @@ -145,7 +154,12 @@ async def create(self: Self, source: Path | URLStr, *, accept_license: bool = Fa
else:
file = await self._bundle_retriever.download_external_bundle(source)

response = await self._requester.post_files("bundles", files={"file": file})
try:
response = await self._requester.post_files("bundles", files={"file": file})
except ConflictError as e:
if "Bundle already exists" in str(e):
raise ObjectAlreadyExistsError(*e.args) from None
raise

bundle = Bundle(requester=self._requester, data=response.as_dict())

Expand Down Expand Up @@ -198,9 +212,12 @@ async def bundle(self: Self) -> Bundle:
# object-specific methods

async def set_ansible_forks(self: Self, value: int) -> Self:
await self._requester.post(
*self.get_own_path(), "ansible-config", data={"config": {"defaults": {"forks": value}}, "adcmMeta": {}}
)
try:
await self._requester.post(
*self.get_own_path(), "ansible-config", data={"config": {"defaults": {"forks": value}}, "adcmMeta": {}}
)
except UnknownError as e:
raise ObjectUpdateError(*e.args) from None
return self

# nodes and managers to access
Expand All @@ -226,14 +243,19 @@ class ClustersNode(PaginatedAccessor[Cluster]):
filtering = Filtering(FilterByName, FilterByBundle, FilterByStatus)

async def create(self: Self, bundle: Bundle, name: str, description: str = "") -> Cluster:
response = await self._requester.post(
"clusters",
data={
"prototypeId": bundle._main_prototype_id,
"name": name,
"description": description,
},
)
try:
response = await self._requester.post(
"clusters",
data={
"prototypeId": bundle._main_prototype_id,
"name": name,
"description": description,
},
)
except ConflictError as e:
if "already exists" in str(e):
raise ObjectAlreadyExistsError(*e.args) from None
raise

return Cluster(requester=self._requester, data=response.as_dict())

Expand Down Expand Up @@ -386,14 +408,19 @@ class HostProvidersNode(PaginatedAccessor[HostProvider]):
filtering = Filtering(FilterByName, FilterByBundle)

async def create(self: Self, bundle: Bundle, name: str, description: str = "") -> HostProvider:
response = await self._requester.post(
"hostproviders",
data={
"prototypeId": bundle._main_prototype_id,
"name": name,
"description": description,
},
)
try:
response = await self._requester.post(
"hostproviders",
data={
"prototypeId": bundle._main_prototype_id,
"name": name,
"description": description,
},
)
except ConflictError as e:
if "duplicate host provider" in str(e):
raise ObjectAlreadyExistsError(*e.args) from None
raise

return HostProvider(requester=self._requester, data=response.as_dict())

Expand Down Expand Up @@ -433,7 +460,12 @@ async def create(
data = {"hostproviderId": hostprovider.id, "name": name, "description": description}
if cluster:
data["clusterId"] = cluster.id
await self._requester.post(*self._path, data=data)
try:
await self._requester.post(*self._path, data=data)
except ConflictError as e:
if "already exists" in str(e):
raise ObjectAlreadyExistsError(*e.args) from None
raise


class HostsInClusterNode(HostsAccessor):
Expand All @@ -446,7 +478,12 @@ def __init__(self: Self, cluster: Cluster) -> None:
async def add(self: Self, host: Host | Iterable[Host] | Filter) -> None:
hosts = await self._get_hosts(host=host, filter_func=self._root_host_filter)

await self._requester.post(*self._path, data=[{"hostId": host.id} for host in hosts])
try:
await self._requester.post(*self._path, data=[{"hostId": host.id} for host in hosts])
except ConflictError as e:
if "already linked to another cluster" in str(e):
raise HostConflictError(*e.args) from None
raise

async def remove(self: Self, host: Host | Iterable[Host] | Filter) -> None:
hosts = await self._get_hosts(host=host, filter_func=self.filter)
Expand Down Expand Up @@ -532,7 +569,7 @@ async def wait(
if timeout:
message = f"{message} in {timeout} seconds with {poll_interval} second interval"

raise TimeoutError(message)
raise WaitTimeoutError(message)

async def terminate(self: Self) -> None:
await self._requester.post(*self.get_own_path(), "terminate", data={})
Expand Down
Loading