Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef4f57a1bd |
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,20 +1,33 @@
|
||||
# Список изменений в версии 13.0.1
|
||||
# Список изменений в версии 13.0.2
|
||||
|
||||
## Изменено
|
||||
| Идентификатор<br>задачи | Описание |
|
||||
| --- | --- |
|
||||
|
||||
|
||||
## Добавлено
|
||||
### Глобально
|
||||
| Идентификатор<br>задачи | Описание |
|
||||
| --- | --- |
|
||||
| BANS-1463 | Добавлен модуль `decort_vm_folder_list`, позволяющий получить список папок для группировки ВМ в ресурсной группе. |
|
||||
| BANS-1467 | Добавлен модуль `decort_vm_folder_access`, позволяющий управлять правами доступа к папке виртуальных машин. |
|
||||
| BANS-1464 | Добавлен модуль `decort_vm_folder`, позволяющий привести к целевому состоянию и получить информацию о папке для группировки ВМ в ресурсной группе. |
|
||||
|
||||
### Модуль decort_vm
|
||||
| Идентификатор<br>задачи | Описание |
|
||||
| --- | --- |
|
||||
| BANS-1413 | Добавлен параметр `disks.objects.read_only`. |
|
||||
| BANS-1466 | Добавлены параметр и возвращаемое значение `folder_id`. |
|
||||
### Модуль decort_vm_list
|
||||
| Идентификатор<br>задачи | Описание |
|
||||
| --- | --- |
|
||||
| BANS-1468 | Добавлено возвращаемое значение `folder_id`. |
|
||||
| BANS-1476 | Добавлен параметр `include_descendants`. |
|
||||
| BANS-1477 | Добавлен параметр `filter.folder_id` для фильтрации виртуальных машин по идентификатору папки для группировки ВМ в ресурсной группе. |
|
||||
|
||||
### Модуль decort_audit_list
|
||||
| Идентификатор<br>задачи | Описание |
|
||||
| --- | --- |
|
||||
| BANS-1840 | Добавлено возвращаемое значение `vmfolder_id`. |
|
||||
|
||||
## Удалено
|
||||
| Идентификатор<br>задачи | Описание |
|
||||
| --- | --- |
|
||||
|
||||
|
||||
## Исправлено
|
||||
| Идентификатор<br>задачи | Описание |
|
||||
| --- | --- |
|
||||
| BANS-1423 | Модуль `decort_vm` завершал работу ошибкой запроса к API при подключении к ВМ интерфейса типа `EMPTY`. |
|
||||
|
||||
@@ -620,6 +620,12 @@ class decort_vm(DecortController):
|
||||
custom_fields=custom_fields_fields,
|
||||
)
|
||||
|
||||
if self.aparams['folder_id'] is not None:
|
||||
self.sdk_checkmode(self.api.cloudapi.vmfolder.computes_move)(
|
||||
compute_ids=[self.comp_info['id']],
|
||||
folder_id=self.aparams['folder_id'],
|
||||
)
|
||||
|
||||
# read in Compute facts once more after all initial setup is complete
|
||||
_, self.comp_info, _ = self.compute_find(
|
||||
comp_id=self.comp_id,
|
||||
@@ -679,6 +685,16 @@ class decort_vm(DecortController):
|
||||
**self.compute_update_args,
|
||||
)
|
||||
|
||||
folder_id = self.aparams['folder_id']
|
||||
if (
|
||||
folder_id is not None
|
||||
and folder_id != (self.comp_info.get('folder_id') or 0)
|
||||
):
|
||||
self.sdk_checkmode(self.api.cloudapi.vmfolder.computes_move)(
|
||||
compute_ids=[self.comp_info['id']],
|
||||
folder_id=folder_id,
|
||||
)
|
||||
|
||||
if self.amodule.params['rollback_to'] is not None:
|
||||
self.sdk_checkmode(self.api.cloudapi.compute.snapshot_rollback)(
|
||||
vm_id=self.comp_info['id'],
|
||||
@@ -979,6 +995,7 @@ class decort_vm(DecortController):
|
||||
tech_status="",
|
||||
account_id=0,
|
||||
rg_id=0,
|
||||
folder_id=0,
|
||||
username="",
|
||||
password="",
|
||||
public_ips=[], # direct IPs; this list can be empty
|
||||
@@ -1012,6 +1029,7 @@ class decort_vm(DecortController):
|
||||
ret_dict['tech_status'] = self.comp_info['techStatus']
|
||||
ret_dict['account_id'] = self.comp_info['accountId']
|
||||
ret_dict['rg_id'] = self.comp_info['rgId']
|
||||
ret_dict['folder_id'] = self.comp_info.get('folder_id') or 0
|
||||
if self.comp_info['tags']:
|
||||
ret_dict['tags'] = self.comp_info['tags']
|
||||
# if the VM is an imported VM, then the 'accounts' list may be empty,
|
||||
@@ -1399,6 +1417,9 @@ class decort_vm(DecortController):
|
||||
),
|
||||
),
|
||||
),
|
||||
folder_id=dict(
|
||||
type='int',
|
||||
),
|
||||
id=dict(
|
||||
type='int',
|
||||
default=0,
|
||||
|
||||
235
library/decort_vm_folder.py
Normal file
235
library/decort_vm_folder.py
Normal file
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: decort_vm_folder
|
||||
|
||||
description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501
|
||||
'''
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.decort_utils import (
|
||||
AnsibleModuleParamSet,
|
||||
DecortController,
|
||||
)
|
||||
|
||||
from dynamix_sdk import exceptions as sdk_exceptions
|
||||
from dynamix_sdk import types as sdk_types
|
||||
|
||||
|
||||
class VmFolderState(Enum):
|
||||
absent = 'absent'
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParamSet(AnsibleModuleParamSet):
|
||||
id: int | None
|
||||
name: str | None
|
||||
parent_id: int | None
|
||||
rg_id: int | None
|
||||
state: VmFolderState | None
|
||||
|
||||
|
||||
class DecortVmFolder(DecortController):
|
||||
param_set: ParamSet
|
||||
facts: dict[str, Any]
|
||||
_id: int | None = None
|
||||
_vm_folder_info: sdk_types.CloudapiVmfolderGetResultModel | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(AnsibleModule(**self.amodule_init_args))
|
||||
self.param_set = ParamSet.from_dict(self.amodule.params)
|
||||
self.facts = {}
|
||||
|
||||
@property
|
||||
def amodule_init_args(self) -> dict[str, Any]:
|
||||
return self.pack_amodule_init_args(
|
||||
argument_spec=ParamSet.get_arg_spec(),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
if self._id is None:
|
||||
raise TypeError
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def vm_folder_info(self) -> sdk_types.CloudapiVmfolderGetResultModel:
|
||||
if self._vm_folder_info is None:
|
||||
try:
|
||||
vm_folder_info = self.api.cloudapi.vmfolder.get(
|
||||
folder_id=self.id,
|
||||
)
|
||||
except sdk_exceptions.RequestException as e:
|
||||
if (
|
||||
e.orig_exception.response is not None
|
||||
and e.orig_exception.response.status_code == 404
|
||||
):
|
||||
self.message(
|
||||
self.MESSAGES.obj_not_found(
|
||||
obj='vm_folder',
|
||||
id=self.id,
|
||||
)
|
||||
)
|
||||
self.exit(fail=True)
|
||||
raise e
|
||||
self._vm_folder_info = vm_folder_info
|
||||
return self._vm_folder_info
|
||||
|
||||
@DecortController.handle_sdk_exceptions
|
||||
def run(self) -> None:
|
||||
if self.param_set.id is not None:
|
||||
self._id = self.param_set.id
|
||||
elif self.param_set.name is not None:
|
||||
self._id = self.find_id_by_name()
|
||||
|
||||
if self._id is not None:
|
||||
self.get_info()
|
||||
self.change()
|
||||
else:
|
||||
self.check_amodule_args_for_create()
|
||||
self.create()
|
||||
|
||||
if self.result['changed']:
|
||||
self.get_info()
|
||||
self.exit()
|
||||
|
||||
def find_id_by_name(self) -> int | None:
|
||||
rg_id = self.param_set.rg_id
|
||||
name = self.param_set.name
|
||||
if rg_id is None or name is None:
|
||||
return None
|
||||
|
||||
matches: list[int] = []
|
||||
page = 1
|
||||
page_size = 100
|
||||
while True:
|
||||
folders = self.api.cloudapi.vmfolder.list(
|
||||
rg_id=rg_id,
|
||||
page=page,
|
||||
size=page_size,
|
||||
)
|
||||
for folder in folders.data:
|
||||
if folder.name != name:
|
||||
continue
|
||||
if (
|
||||
self.param_set.parent_id is not None
|
||||
and folder.parent_id != self.param_set.parent_id
|
||||
):
|
||||
continue
|
||||
matches.append(folder.id)
|
||||
|
||||
if not folders.data or len(folders.data) < page_size:
|
||||
break
|
||||
if (
|
||||
folders.entry_count
|
||||
and page * page_size >= folders.entry_count
|
||||
):
|
||||
break
|
||||
page += 1
|
||||
|
||||
if len(matches) > 1:
|
||||
self.message(
|
||||
msg='Check for parameter "name" failed: '
|
||||
'multiple vm folders with this name were found; '
|
||||
'specify parent_id or id'
|
||||
)
|
||||
self.exit(fail=True)
|
||||
if matches:
|
||||
return matches[0]
|
||||
return None
|
||||
|
||||
def get_info(self) -> None:
|
||||
if self._vm_folder_info is not None and not self.amodule.check_mode:
|
||||
self._vm_folder_info = None
|
||||
self.facts = self.vm_folder_info.model_dump()
|
||||
|
||||
def check_amodule_args_for_create(self) -> None:
|
||||
check_errors = False
|
||||
|
||||
if self.param_set.rg_id is None:
|
||||
check_errors = True
|
||||
self.message(
|
||||
msg='Check for parameter "rg_id" failed: '
|
||||
'rg_id must be specified when creating '
|
||||
'a new vm_folder'
|
||||
)
|
||||
|
||||
if self.param_set.name is None:
|
||||
check_errors = True
|
||||
self.message(
|
||||
msg='Check for parameter "name" failed: '
|
||||
'name must be specified when creating '
|
||||
'a new vm_folder'
|
||||
)
|
||||
|
||||
if self.param_set.state == VmFolderState.absent:
|
||||
check_errors = True
|
||||
self.message(
|
||||
msg='Check for parameter "state" failed: '
|
||||
'state can not be "absent" when creating '
|
||||
'a new vm_folder'
|
||||
)
|
||||
|
||||
if check_errors:
|
||||
self.exit(fail=True)
|
||||
|
||||
def create(self) -> None:
|
||||
rg_id = self.param_set.rg_id
|
||||
name = self.param_set.name
|
||||
if rg_id is None or name is None:
|
||||
raise TypeError
|
||||
created_id = self.sdk_checkmode(
|
||||
self.api.cloudapi.vmfolder.create
|
||||
)(
|
||||
rg_id=rg_id,
|
||||
name=name,
|
||||
parent_id=self.param_set.parent_id,
|
||||
)
|
||||
if created_id:
|
||||
self._id = created_id
|
||||
|
||||
def change(self) -> None:
|
||||
self.change_state()
|
||||
self.change_params()
|
||||
|
||||
def change_state(self) -> None:
|
||||
if self.param_set.state == VmFolderState.absent:
|
||||
self.delete()
|
||||
|
||||
def change_params(self) -> None:
|
||||
if (
|
||||
self.param_set.name is not None
|
||||
and self.param_set.name != self.vm_folder_info.name
|
||||
):
|
||||
self.sdk_checkmode(self.api.cloudapi.vmfolder.rename)(
|
||||
folder_id=self.id,
|
||||
name=self.param_set.name,
|
||||
)
|
||||
if (
|
||||
self.param_set.parent_id is not None
|
||||
and self.param_set.parent_id != self.vm_folder_info.parent_id
|
||||
):
|
||||
self.sdk_checkmode(self.api.cloudapi.vmfolder.move)(
|
||||
folder_id=self.id,
|
||||
new_parent_id=self.param_set.parent_id,
|
||||
)
|
||||
|
||||
def delete(self) -> None:
|
||||
self.sdk_checkmode(self.api.cloudapi.vmfolder.delete)(
|
||||
folder_id=self.id,
|
||||
)
|
||||
self.exit()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
DecortVmFolder().run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
141
library/decort_vm_folder_access.py
Normal file
141
library/decort_vm_folder_access.py
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: decort_vm_folder_access
|
||||
|
||||
description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501
|
||||
'''
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.decort_utils import (
|
||||
AnsibleModuleParamSet,
|
||||
DecortController,
|
||||
)
|
||||
|
||||
from dynamix_sdk import exceptions as sdk_exceptions
|
||||
from dynamix_sdk import types as sdk_types
|
||||
|
||||
|
||||
class VMFolderAccessState(Enum):
|
||||
absent = 'absent'
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParamSet(AnsibleModuleParamSet):
|
||||
folder_id: int
|
||||
user: str
|
||||
right: sdk_types.AccessTypeForSet | None
|
||||
state: VMFolderAccessState | None
|
||||
|
||||
|
||||
class DecortVMFolderAccess(DecortController):
|
||||
param_set: ParamSet
|
||||
folder_info: sdk_types.CloudapiVmfolderGetResultModel
|
||||
acl: sdk_types.ACLForVMFolderGetAPIResultNM | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(AnsibleModule(**self.amodule_init_args))
|
||||
self.param_set = ParamSet.from_dict(self.amodule.params)
|
||||
|
||||
@property
|
||||
def amodule_init_args(self) -> dict[str, Any]:
|
||||
return self.pack_amodule_init_args(
|
||||
argument_spec=ParamSet.get_arg_spec(),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
@DecortController.handle_sdk_exceptions
|
||||
def run(self) -> None:
|
||||
self.get_folder()
|
||||
self.check_amodule_args()
|
||||
self.change()
|
||||
if self.result['changed'] and not self.amodule.check_mode:
|
||||
self.get_folder()
|
||||
self.package_facts()
|
||||
self.exit()
|
||||
|
||||
def get_folder(self) -> None:
|
||||
try:
|
||||
self.folder_info = self.api.cloudapi.vmfolder.get(
|
||||
folder_id=self.param_set.folder_id,
|
||||
)
|
||||
except sdk_exceptions.RequestException as e:
|
||||
if (
|
||||
e.orig_exception.response is not None
|
||||
and e.orig_exception.response.status_code == 404
|
||||
):
|
||||
self.message(
|
||||
self.MESSAGES.obj_not_found(
|
||||
obj='VM folder',
|
||||
id=self.param_set.folder_id,
|
||||
)
|
||||
)
|
||||
self.exit(fail=True)
|
||||
raise e
|
||||
self.acl = self.find_acl()
|
||||
|
||||
def find_acl(self) -> sdk_types.ACLForVMFolderGetAPIResultNM | None:
|
||||
for acl in self.folder_info.acl:
|
||||
if acl.explicit and acl.user_group_id == self.param_set.user:
|
||||
return acl
|
||||
return None
|
||||
|
||||
def check_amodule_args(self) -> None:
|
||||
if (
|
||||
self.param_set.state is None
|
||||
and self.param_set.right is None
|
||||
and self.acl is None
|
||||
):
|
||||
self.message(
|
||||
self.MESSAGES.obj_not_found(
|
||||
obj='VM folder ACL',
|
||||
id=self.param_set.user,
|
||||
)
|
||||
)
|
||||
self.exit(fail=True)
|
||||
|
||||
def change(self) -> None:
|
||||
if self.param_set.state == VMFolderAccessState.absent:
|
||||
self.delete()
|
||||
return
|
||||
|
||||
right = self.param_set.right
|
||||
if right is None or (
|
||||
self.acl is not None and self.acl.right == right
|
||||
):
|
||||
return
|
||||
|
||||
self.sdk_checkmode(self.api.cloudapi.vmfolder.add_acl_access)(
|
||||
folder_id=self.param_set.folder_id,
|
||||
user=self.param_set.user,
|
||||
right=right,
|
||||
)
|
||||
|
||||
def delete(self) -> None:
|
||||
if self.acl is None:
|
||||
return
|
||||
self.sdk_checkmode(self.api.cloudapi.vmfolder.del_acl_access)(
|
||||
folder_id=self.param_set.folder_id,
|
||||
user=self.param_set.user,
|
||||
)
|
||||
if not self.amodule.check_mode:
|
||||
self.acl = None
|
||||
|
||||
def package_facts(self) -> None:
|
||||
if self.acl is not None:
|
||||
self.facts = self.acl.model_dump()
|
||||
else:
|
||||
self.facts = {}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
DecortVMFolderAccess().run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
71
library/decort_vm_folder_list.py
Normal file
71
library/decort_vm_folder_list.py
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: decort_vm_folder_list
|
||||
|
||||
description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501
|
||||
'''
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.decort_utils import (
|
||||
AnsibleModuleParamSet,
|
||||
DecortController,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FilterParam:
|
||||
rg_id: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PaginationParam:
|
||||
page: int | None
|
||||
size: int | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParamSet(AnsibleModuleParamSet):
|
||||
filter: FilterParam
|
||||
pagination: PaginationParam
|
||||
|
||||
|
||||
class DecortVMFolderList(DecortController):
|
||||
param_set: ParamSet
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(AnsibleModule(**self.amodule_init_args))
|
||||
self.param_set = ParamSet.from_dict(self.amodule.params)
|
||||
|
||||
@property
|
||||
def amodule_init_args(self) -> dict:
|
||||
return self.pack_amodule_init_args(
|
||||
argument_spec=ParamSet.get_arg_spec(),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
@DecortController.handle_sdk_exceptions
|
||||
def run(self):
|
||||
self.get_info()
|
||||
self.exit()
|
||||
|
||||
def get_info(self):
|
||||
aparam_filter = self.param_set.filter
|
||||
aparam_pagination = self.param_set.pagination
|
||||
|
||||
self.facts = self.api.cloudapi.vmfolder.list(
|
||||
rg_id=aparam_filter.rg_id,
|
||||
page=aparam_pagination.page,
|
||||
size=aparam_pagination.size,
|
||||
).model_dump()['data']
|
||||
|
||||
|
||||
def main():
|
||||
DecortVMFolderList().run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -36,12 +36,18 @@ class DecortVMList(DecortController):
|
||||
ext_net_name=dict(
|
||||
type='str',
|
||||
),
|
||||
folder_id=dict(
|
||||
type='int',
|
||||
),
|
||||
id=dict(
|
||||
type='int',
|
||||
),
|
||||
include_deleted=dict(
|
||||
type='bool',
|
||||
),
|
||||
include_descendants=dict(
|
||||
type='bool',
|
||||
),
|
||||
ip_addr=dict(
|
||||
type='str',
|
||||
),
|
||||
@@ -129,8 +135,10 @@ class DecortVMList(DecortController):
|
||||
account_id=aparam_filter['account_id'],
|
||||
ext_net_id=aparam_filter['ext_net_id'],
|
||||
ext_net_name=aparam_filter['ext_net_name'],
|
||||
folder_id=aparam_filter['folder_id'],
|
||||
id=aparam_filter['id'],
|
||||
include_deleted=aparam_filter['include_deleted'] or False,
|
||||
include_descendants=aparam_filter['include_descendants'] or False,
|
||||
ip_addr=aparam_filter['ip_addr'],
|
||||
name=aparam_filter['name'],
|
||||
rg_id=aparam_filter['rg_id'],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from copy import deepcopy
|
||||
from dataclasses import Field, MISSING, dataclass, fields, is_dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from functools import wraps
|
||||
@@ -6,19 +7,24 @@ from importlib.metadata import version as get_package_version
|
||||
import inspect
|
||||
import json
|
||||
import re
|
||||
from types import UnionType
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Iterable,
|
||||
Literal,
|
||||
Optional,
|
||||
ParamSpec,
|
||||
TypeVar,
|
||||
cast,
|
||||
get_args,
|
||||
get_origin,
|
||||
)
|
||||
import time
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
import dacite
|
||||
from dynamix_sdk import BVSAuth, DECS3OAuth, Dynamix
|
||||
from dynamix_sdk import __name__ as SDK_PACKAGE_NAME
|
||||
from dynamix_sdk import exceptions as sdk_exceptions
|
||||
@@ -33,6 +39,152 @@ P = ParamSpec('P')
|
||||
R = TypeVar('R')
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AnsibleModuleParamSet:
|
||||
"""Dataclass that can generate AnsibleModule argument_spec."""
|
||||
|
||||
_ANSIBLE_TYPE_BY_PYTHON: ClassVar[dict[type, str]] = {
|
||||
bool: 'bool',
|
||||
int: 'int',
|
||||
str: 'str',
|
||||
}
|
||||
_ALLOWED_FIELD_METADATA_KEYS: ClassVar[frozenset[str]] = frozenset({'choices'}) # noqa: E501
|
||||
|
||||
@classmethod
|
||||
def get_arg_spec(cls) -> dict[str, Any]:
|
||||
return cls._dataclass_to_arg_spec(cls)
|
||||
|
||||
@classmethod
|
||||
def from_dict[T: AnsibleModuleParamSet](
|
||||
cls: type[T], data: dict[str, Any]
|
||||
) -> T:
|
||||
return dacite.from_dict(
|
||||
data_class=cls,
|
||||
data=data,
|
||||
config=dacite.Config(cast=[Enum]),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _dataclass_to_arg_spec(cls, model_cls: type) -> dict[str, Any]:
|
||||
spec: dict[str, Any] = {}
|
||||
for field in fields(model_cls):
|
||||
spec[field.name] = cls._field_to_arg_spec(
|
||||
field=field,
|
||||
model_cls=model_cls,
|
||||
)
|
||||
return spec
|
||||
|
||||
@classmethod
|
||||
def _field_to_arg_spec(
|
||||
cls,
|
||||
field: Field,
|
||||
model_cls: type,
|
||||
) -> dict[str, Any]:
|
||||
if field.default is None:
|
||||
raise ValueError(
|
||||
f'{model_cls.__name__}.{field.name}: '
|
||||
'default=None is not allowed'
|
||||
)
|
||||
|
||||
if get_origin(field.type) is UnionType:
|
||||
args = get_args(field.type)
|
||||
if len(args) != 2 or type(None) not in args:
|
||||
raise ValueError(
|
||||
f'{model_cls.__name__}.{field.name}: '
|
||||
f'unsupported annotation {field.type!r}: '
|
||||
'expected T | None'
|
||||
)
|
||||
param_type = args[0] if args[1] is type(None) else args[1]
|
||||
allows_none = True
|
||||
else:
|
||||
param_type = field.type
|
||||
allows_none = False
|
||||
has_default = (
|
||||
field.default is not MISSING
|
||||
or field.default_factory is not MISSING
|
||||
)
|
||||
|
||||
unknown_metadata_keys = (
|
||||
set(field.metadata) - cls._ALLOWED_FIELD_METADATA_KEYS
|
||||
)
|
||||
if unknown_metadata_keys:
|
||||
raise ValueError(
|
||||
f'{model_cls.__name__}.{field.name}: '
|
||||
f'unsupported metadata keys {sorted(unknown_metadata_keys)!r}'
|
||||
)
|
||||
|
||||
try:
|
||||
entry = cls._type_to_arg_spec(param_type)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f'{model_cls.__name__}.{field.name}: {exc}'
|
||||
)
|
||||
|
||||
if entry['type'] == 'dict' and 'options' in entry:
|
||||
if not allows_none:
|
||||
entry['apply_defaults'] = True
|
||||
return entry
|
||||
|
||||
metadata_choices = field.metadata.get('choices')
|
||||
if metadata_choices is not None:
|
||||
entry['choices'] = list(metadata_choices)
|
||||
|
||||
if not allows_none and not has_default:
|
||||
entry['required'] = True
|
||||
elif has_default:
|
||||
entry['default'] = cls._resolve_default(field)
|
||||
|
||||
return entry
|
||||
|
||||
@classmethod
|
||||
def _type_to_arg_spec(cls, type_: Any) -> dict[str, Any]:
|
||||
if get_origin(type_) is list:
|
||||
list_args = get_args(type_)
|
||||
if len(list_args) != 1:
|
||||
raise ValueError(f'unsupported annotation {type_!r}')
|
||||
elem_spec = cls._type_to_arg_spec(list_args[0])
|
||||
entry: dict[str, Any] = {
|
||||
'type': 'list',
|
||||
'elements': elem_spec['type'],
|
||||
}
|
||||
if 'options' in elem_spec:
|
||||
entry['options'] = elem_spec['options']
|
||||
if 'choices' in elem_spec:
|
||||
entry['choices'] = elem_spec['choices']
|
||||
return entry
|
||||
|
||||
if isinstance(type_, type) and is_dataclass(type_):
|
||||
return {
|
||||
'type': 'dict',
|
||||
'options': cls._dataclass_to_arg_spec(type_),
|
||||
}
|
||||
if isinstance(type_, type) and issubclass(type_, Enum):
|
||||
return {
|
||||
'type': 'str',
|
||||
'choices': list(type_._member_names_),
|
||||
}
|
||||
if get_origin(type_) is Literal:
|
||||
return {
|
||||
'type': 'str',
|
||||
'choices': list(get_args(type_)),
|
||||
}
|
||||
if type_ in cls._ANSIBLE_TYPE_BY_PYTHON:
|
||||
return {'type': cls._ANSIBLE_TYPE_BY_PYTHON[type_]}
|
||||
raise ValueError(f'unsupported annotation {type_!r}')
|
||||
|
||||
@staticmethod
|
||||
def _resolve_default(field: Field) -> Any:
|
||||
if field.default is not MISSING:
|
||||
default = field.default
|
||||
elif field.default_factory is not MISSING:
|
||||
default = field.default_factory()
|
||||
else:
|
||||
raise ValueError(f'{field.name}: no default to resolve')
|
||||
if isinstance(default, Enum):
|
||||
return default.name
|
||||
return default
|
||||
|
||||
|
||||
class DecortController(object):
|
||||
"""DecortController is a utility class that holds target controller context and handles API requests formatting
|
||||
based on the requested authentication type.
|
||||
@@ -53,7 +205,7 @@ class DecortController(object):
|
||||
_api: sdk_types.API | None = None
|
||||
_usermanager_whoami_result: None | dict = None
|
||||
|
||||
ANSIBLE_MODULES_VERSION = '13.0.1'
|
||||
ANSIBLE_MODULES_VERSION = '13.0.2'
|
||||
COMPATIBLE_SDK_MINOR_VERSION = '2.0'
|
||||
|
||||
VM_RESIZE_NOT = 0
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
ansible==11.6.0
|
||||
dacite==1.9.2
|
||||
requests==2.32.3
|
||||
git+https://repository.basistech.ru/BASIS/dynamix-python-sdk.git@2.0.latest
|
||||
|
||||
Reference in New Issue
Block a user