This commit is contained in:
Vladislav Nesterov
2026-09-25 22:25:56 +07:00
parent cd6a835227
commit ef4f57a1bd
8 changed files with 652 additions and 10 deletions

View File

@@ -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
View 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()

View 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()

View 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()

View File

@@ -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'],