From 1ab446e05d24c1a288df1102a3c54f4923098e84 Mon Sep 17 00:00:00 2001 From: sskarimov Date: Mon, 1 Jun 2026 18:27:15 +0300 Subject: [PATCH] 12.0.0 --- CHANGELOG.md | 171 +- README.md | 1 + library/decort_account.py | 439 ++- library/decort_bservice.py | 25 +- library/decort_disk.py | 105 +- library/decort_disk_list.py | 15 +- library/decort_group.py | 4 +- library/decort_image.py | 31 +- library/decort_k8s.py | 125 +- library/decort_lb.py | 239 +- library/decort_pfw.py | 52 +- library/decort_rg.py | 206 +- library/decort_sdn_access_group.py | 213 ++ library/decort_sdn_access_group_list.py | 120 + library/decort_sdn_hypervisor.py | 160 ++ library/decort_sdn_hypervisor_list.py | 135 + library/decort_sdn_logical_port.py | 588 ++++ library/decort_sdn_logical_port_address.py | 206 ++ library/decort_sdn_logical_port_list.py | 171 ++ library/decort_sdn_network_object_group.py | 299 +++ ...ecort_sdn_network_object_group_ip_range.py | 209 ++ ...t_sdn_network_object_group_logical_port.py | 245 ++ .../decort_sdn_network_object_group_mac.py | 166 ++ library/decort_sdn_segment.py | 357 +++ library/decort_sdn_segment_list.py | 142 + library/decort_security_group.py | 2 +- library/decort_storage_policy.py | 2 +- library/decort_trunk.py | 2 +- library/decort_user.py | 33 +- library/decort_vins.py | 371 ++- library/decort_vm.py | 57 +- library/decort_zone.py | 2 +- module_utils/decort_utils.py | 2353 ++++++----------- requirements.txt | 2 +- 34 files changed, 5135 insertions(+), 2113 deletions(-) create mode 100644 library/decort_sdn_access_group.py create mode 100644 library/decort_sdn_access_group_list.py create mode 100644 library/decort_sdn_hypervisor.py create mode 100644 library/decort_sdn_hypervisor_list.py create mode 100644 library/decort_sdn_logical_port.py create mode 100644 library/decort_sdn_logical_port_address.py create mode 100644 library/decort_sdn_logical_port_list.py create mode 100644 library/decort_sdn_network_object_group.py create mode 100644 library/decort_sdn_network_object_group_ip_range.py create mode 100644 library/decort_sdn_network_object_group_logical_port.py create mode 100644 library/decort_sdn_network_object_group_mac.py create mode 100644 library/decort_sdn_segment.py create mode 100644 library/decort_sdn_segment_list.py diff --git a/CHANGELOG.md b/CHANGELOG.md index dd2aa3e..f35477d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,174 @@ -# Список изменений в версии 11.0.3 +# Список изменений в версии 12.0.0 ## Добавлено +### Глобально +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1164 | Добавлен модуль `decort_sdn_hypervisor_list`, позволяющий получить список гипервизоров SDN.| +| BANS-1162 | Добавлен модуль `decort_sdn_access_group_list`, позволяющий получить список групп доступа SDN.| +| BANS-1161 | Добавлен модуль `decort_sdn_segment_list`, позволяющий получить список доступных сегментов SDN. | +| BANS-1154 | Добавлен модуль `decort_sdn_segment`, позволяющий привести к целевому состоянию и получить информацию о сегментах SDN. | +| BANS-1158 | Добавлен модуль `decort_sdn_hypervisor`, позволяющий привести к целевому состоянию и получить информацию о гипервизоре SDN. | +| BANS-1156 | Добавлен модуль `decort_sdn_access_group`, позволяющий создать, привести к целевому состоянию и получить информацию о группе доступа SDN. | +| BANS-1163 | Добавлен модуль `decort_sdn_logical_port_list`, позволяющий получить список доступных логических портов SDN. | +| BANS-1159 | Добавлен модуль `decort_sdn_network_object_group`, позволяющий привести к целевому состоянию и получить информацию о группах сетевых объектов. | +| BANS-1157 | Добавлен модуль `decort_sdn_logical_port`, позволяющий привести к целевому состоянию и получить информацию о логическом порте. | +| BANS-1184 | Добавлен модуль `decort_sdn_logical_port_address`, позволяющий привести к целевому состоянию и получить информацию об адресе логического порта SDN. | +| BANS-1186 | Добавлен модуль `decort_sdn_network_object_group_logical_port`, позволяющий привести к целевому состоянию логические порты сетевых групп объектов SDN. | +| BANS-1185 | Добавлен модуль `decort_sdn_network_object_group_ip_range`, позволяющий привести к целевому состоянию и получить информацию о диапазоне IP-адресов в группе сетевых объектов SDN. | +| BANS-1193 | Добавлен модуль `decort_sdn_network_object_group_mac`, позволяющий привести к целевому состоянию и получить информацию о MAC-адресе в группе сетевых объектов SDN. | -## Удалено +### Модуль decort_disk +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1011 | Добавлены возвращаемые значения `account_name`, `acl`, `created_by`, `created_datetime`, `created_timestamp`, `deleted_by`, `deleted_datetime`, `deleted_timestamp`, `description`, `destruction_datetime`, `destruction_timestamp`, `device_name`, `image_id`, `image_ids`, `milestones`, `params`, `parent_id`, `present_to`, `purge_datetime`, `purge_timestamp`, `replication`, `res_id`, `res_name`, `role`, `sep_type`, `shared`, `snapshots`, `tech_status`, `updated_by`, `updated_datetime`, `updated_timestamp`. | +| BANS-1104 | Добавлено возвращаемое значение `provision`. | -## Исправлено ### Модуль decort_k8s | Идентификатор
задачи | Описание | | --- | --- | -| BANS-1208 | Модуль завершал работу ошибкой запроса к API при попытке модуля пересоздать группу воркеров. | +| BANS-238 | Добавлена возможность изменить описание объекта. | +| BANS-1134 | Добавлены возвращаемые значения `account_name`, `acl`, `acl.account`, `acl.k8s`, `acl.rg`, `bservice_id`, `created_by`, `created_datetime`, `created_timestamp`, `deleted_by`, `deleted_datetime`, `deleted_timestamp`, `extnet_only`, `k8ci_id`, `k8ci_name`, `lb_ha_ips`, `lb_ha_ips.backend`, `lb_ha_ips.frontend`, `lb_ha_mode`, `network_plugin`, `node_groups`, `node_groups.master`, `node_groups.master.id`, `node_groups.master.name`, `node_groups.master.annotations`, `node_groups.master.labels`, `node_groups.master.taints`, `node_groups.master.vms.ext_ip`, `node_groups.master.vms.name`, `node_groups.master.vms.status`, `node_groups.worker`, `node_groups.worker.id`, `node_groups.worker.name`, `node_groups.worker.annotations`, `node_groups.worker.labels`, `node_groups.worker.taints`, `node_groups.worker.vms.ext_ip`, `node_groups.worker.vms.name`, `node_groups.worker.vms.status`, `rg_name`, `updated_by`, `updated_datetime`, `updated_timestamp`, `with_lb`. | + +### Модуль decort_rg +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1063 | Добавлены возвращаемые значения `account_name`, `acl`, `cpu_allocation_parameter`, `cpu_allocation_ratio`, `created_by`, `created_timestamp`, `created_datetime`, `deleted_by`, `deleted_timestamp`, `deleted_datetime`, `dirty`, `guid`, `lock_status`, `milestones`, `secret`, `updated_by`, `updated_timestamp`, `updated_datetime`, `vm_features`. | + +### Модуль decort_vins +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1061 | Добавлены возвращаемые значения `account_name`, `created_by`, `created_datetime`, `created_timestamp`, `default_gw`, `default_qos`, `deleted_by`, `deleted_datetime`, `deleted_timestamp`, `description`, `guid`, `lock_status`, `manager_id`, `manager_type`, `milestones`, `net_prefix`, `pre_reservation_count`, `redundant`, `rg_name`, `secondary_vnfdev_id`, `security_group_mode`, `updated_by`, `updated_datetime`, `updated_timestamp`, `user_managed`, `vms`, `vnfdev`, `vnfs`, `vxlan_id`. | +| BANS-1132 | Добавлен параметр `security_group_mode`. | + +### Модуль decort_vm +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1088 | Добавлено возвращаемое значение `weight`. | + +### Модуль decort_disk_list +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1083 | Добавлен параметр `filter.vm_id`. | +| BANS-1084 | Добавлен параметр `filter.rg_id`. | +| BANS-1096 | Добавлено возвращаемое значение `independent`. | +| BANS-1103 | Добавлено возвращаемое значение `provision`. | + +### Модуль decort_image +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1094 | Добавлено возвращаемое значение `independent`. | +| BANS-1108 | Добавлено возвращаемое значение `links_to`. | + +### Модуль decort_zone_list +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1116 | Добавлено возвращаемое значение `drs_dx_app_id`. | +| BANS-1117 | Добавлено возвращаемое значение `drs_dx_url`. | +| BANS-1118 | Добавлено возвращаемое значение `drs`. | +| BANS-1119 | Добавлено возвращаемое значение `drs_name`. | +| BANS-1120 | Добавлено возвращаемое значение `drs_uid`. | +| BANS-1121 | Добавлено возвращаемое значение `drs_dx_sso_url`. | +| BANS-1147 | Добавлено возвращаемое значение `drs_dx_ssl_skip_verify`. | +| BANS-1148 | Добавлено возвращаемое значение `drs_bvs_domain`. | +| BANS-1149 | Добавлено возвращаемое значение `drs_broadcast_ip_addr`. | +| BANS-1166 | Добавлено возвращаемое значение `drs_dx_sso_type`. | +| BANS-1150 | Добавлено возвращаемое значение `drs_ping_ip_addr`. | + +### Модуль decort_zone +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1124 | Добавлено возвращаемое значение `drs_dx_app_id`. | +| BANS-1122 | Добавлено возвращаемое значение `drs`. | +| BANS-1125 | Добавлено возвращаемое значение `drs_dx_url`. | +| BANS-1123 | Добавлено возвращаемое значение `drs_uid`. | +| BANS-1126 | Добавлено возвращаемое значение `drs_name`. | +| BANS-1127 | Добавлено возвращаемое значение `drs_dx_sso_url`. | +| BANS-1143 | Добавлено возвращаемое значение `drs_dx_ssl_skip_verify`. | +| BANS-1144 | Добавлено возвращаемое значение `drs_bvs_domain`. | +| BANS-1145 | Добавлено возвращаемое значение `drs_broadcast_ip_addr`. | +| BANS-1146 | Добавлено возвращаемое значение `drs_ping_ip_addr`. | +| BANS-1165 | Добавлено возвращаемое значение `drs_dx_sso_type`. | + +### Модуль decort_lb +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1112 | Добавлены возвращаемые значения `acl`, `backend_ha_ip_addr`, `created_by`, `created_datetime`, `created_timestamp`, `deleted_by`, `deleted_datetime`, `deleted_timestamp`, `description`, `dp_api_user`, `ext_net_id`, `frontend_ha_ip_addr`, `guid`, `ha_mode`, `manager_id`, `manager_type`, `milestones`, `part_of_k8s`, `primary_node.backend_ip_addr`, `primary_node.frontend_ip_addr`, `primary_node.guid`, `primary_node.mgmt_ip`, `primary_node.net_id`, `primary_node.vm_id`, `rg_name`, `secondary_node.backend_ip_addr`, `secondary_node.frontend_ip_addr`, `secondary_node.guid`, `secondary_node.mgmt_ip`, `secondary_node.net_id`, `secondary_node.vm_id`, `updated_by`, `updated_datetime`, `updated_timestamp`, `user_managed`, `vins_id`. | + +### Модуль decort_user +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1176 | Добавлены возвращаемые значения `consumed.storage_policies.storage_size_quota_gb`, `reserved.storage_policies.storage_size_quota_gb`. | + +## Удалено +### Глобально +| Идентификатор
задачи | Описание | +| --- | --- | +### Модуль decort_account +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1040 | Удалены возвращаемые значения `computes_amount` , `createdTime`, `createdTime_readable`, `deactivationTime`, `deactivationTime_readable`, `deletedTime`, `deletedTime_readable`, `resourceLimits`, `resourceLimits.CU_C`, `resourceLimits.CU_D`, `resourceLimits.CU_DM`, `resourceLimits.CU_I`, `resourceLimits.CU_M`, `resourceLimits.gpu_units`, `updatedTime`, `updatedTime_readable`, `vinses_amount` в связи с переименованием в `vm_counts` , `created_timestamp`, `created_datetime`, `deactivation_timestamp`, `deactivation_datetime`, `deleted_timestamp`, `deleted_datetime`, `quotas`, `quotas.cpu_count`, `quotas.disk_size_gb`, `quotas.storage_size_gb`, `quotas.ext_ip_count`, `quotas.ram_size_mb`, `quotas.gpu_count`, `updated_timestamp`, `updated_datetime`, `vins_count`. | +| BANS-1067 | Удалены параметры `access_emails`, `quotas.cpu`, `quotas.disk_size`, `quotas.gpu`, `quotas.public_ip`, `quotas.ram` в связи с переименованием в `send_access_emails`, `quotas.cpu_count`, `quotas.storage_size_gb`, `quotas.gpu_count`, `quotas.ext_ip_count`, `quotas.ram_size_mb`. | + +### Модуль decort_disk +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1011 | Удалены возвращаемые значения `computes`, `gid`, `iotune`, `pool`, `size`, `size_available`, `size_used`, `state` в связи с переименованием в `vms`, `grid_id`, `io_tune`, `sep_pool_name`, `size_max_gb`, `size_available_gb`, `size_used_gb`, `status` | + +### Модуль decort_k8s +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1068 | Для параметра `description` удалено значение по умолчанию. | +| BANS-1134 | Удалены возвращаемые значения `techStatus`, `state`, `k8s_Masters`, `k8s_Masters.cpu`, `k8s_Masters.ram`, `k8s_Masters.disk`, `k8s_Masters.num`, `k8s_Masters.detailedInfo`, `k8s_Masters.detailedInfo.techStatus`, `k8s_Workers`, `k8s_Workers.cpu`, `k8s_Workers.ram`, `k8s_Workers.disk`, `k8s_Workers.num`, `k8s_Workers.detailedInfo`, `k8s_Workers.detailedInfo.techStatus` в связи с переименованием в `tech_status`, `status`, `node_groups.master`, `node_groups.master.node_cpu_count`, `node_groups.master.node_ram_size_mb`, `node_groups.master.node_boot_disk_size_gb`, `node_groups.master.node_count`, `node_groups.master.vms`, `node_groups.master.vms.tech_status`, `node_groups.worker`, `node_groups.worker.node_cpu_count`, `node_groups.worker.node_ram_size_mb`, `node_groups.worker.node_boot_disk_size_gb`, `node_groups.worker.node_count`, `node_groups.worker.vms`, `node_groups.worker.vms.tech_status`. | + +### Модуль decort_rg +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1063 | Удалены возвращаемые значения `computes`, `defNetId`, `defNetType`, `gid`, `quota`, `resTypes`, `uniqPools`, `ViNS` в связи с переименованием в `vm_ids`, `default_net_id`, `default_net_type`, `grid_id`, `quotas`, `resource_types`, `sep_pools`, `vins_ids` | + +### Модуль decort_vins +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1061 | Удалены возвращаемые значения `int_net_addr`, `gid`, `state` в связи с переименованием в `net_ip`, `grid_id`, `status`. | +| BANS-1061 | Удалены возвращаемые значения `custom_net_addr`, `ext_ip_addr`, `ssh_ipaddr`, `ssh_password`, `ssh_port`. | +| BANS-1054 | Для параметра `state` значение по умолчанию изменено на значение по умолчанию если объект не существует или безвозвратно удалён. | + +### Модуль decort_disk_list +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1085 | Удалён параметр `filter.type`. | +| BANS-1086 | Удалено возвращаемое значение `type`. | + +### Модуль decort_user +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1080 | Удалено возвращаемое значение `emailaddresses` в связи с переименованием в `email_addresses`. | +| BANS-1176 | Удалены возвращаемые значения `resource_consumed`, `resource_consumed.cpu`, `resource_consumed.disksize`, `resource_consumed.disksizemax`, `resource_consumed.extips`, `resource_consumed.gpu`, `resource_consumed.ram`, `resource_consumed.policies`, `resource_consumed.policies.disksize`, `resource_consumed.policies.disksizemax`, `resource_consumed.policies.seps.disksize`, `resource_consumed.policies.seps.disksizemax`, `resource_consumed.seps`, `resource_consumed.seps.disksize`, `resource_consumed.seps.disksizemax`, `resource_reserved`, `resource_reserved.cpu`, `resource_reserved.disksize`, `resource_reserved.disksizemax`, `resource_reserved.extips`, `resource_reserved.gpu`, `resource_reserved.ram`, `resource_reserved.policies`, `resource_reserved.policies.disksize`, `resource_reserved.policies.disksizemax`, `resource_reserved.policies.seps.disksize`, `resource_reserved.policies.seps.disksizemax`, `resource_reserved.seps`, `resource_reserved.seps.disksize`, `resource_reserved.seps.disksizemax` в связи с переименованием в `consumed`, `consumed.cpu_count`, `consumed.storage_size_gb_by_real_usage`, `consumed.storage_size_gb_by_disk_max`, `consumed.ext_ip_count`, `consumed.gpu_count`, `consumed.ram_size_mb`, `consumed.storage_policies`, `consumed.storage_policies.storage_size_gb_by_real_usage`, `consumed.storage_policies.storage_size_gb_by_disk_max`, `consumed.storage_policies.sep_pools.storage_size_gb_by_real_usage`, `consumed.storage_policies.sep_pools.storage_size_gb_by_disk_max`, `consumed.sep_pools`, `consumed.sep_pools.storage_size_gb_by_real_usage`, `consumed.sep_pools.storage_size_gb_by_disk_max`, `reserved`, `reserved.cpu_count`, `reserved.storage_size_gb_by_real_usage`, `reserved.storage_size_gb_by_disk_max`, `reserved.ext_ip_count`, `reserved.gpu_count`, `reserved.ram_size_mb`, `reserved.storage_policies`, `reserved.storage_policies.storage_size_gb_by_real_usage`, `reserved.storage_policies.storage_size_gb_by_disk_max`, `reserved.storage_policies.sep_pools.storage_size_gb_by_real_usage`, `reserved.storage_policies.sep_pools.storage_size_gb_by_disk_max`, `reserved.sep_pools`, `reserved.sep_pools.storage_size_gb_by_real_usage`, `reserved.sep_pools.storage_size_gb_by_disk_max`. | + +### Модуль decort_lb +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1112 | Удалены возвращаемые значения `backends.serverDefaultSettings`, `backends.servers.address`, `backends.servers.serverSettings`, `frontends.backend`, `frontends.bindings.address`, `gid`, `state`, `sysctl` в связи с переименованием в `backends.server_default_settings`, `backends.servers.ip_addr`, `backends.servers.server_settings`, `frontends.backend_name`, `frontends.bindings.ip_addr`, `grid_id`, `status`, `sysctl_params`. | + +## Исправлено +### Модуль decort_account +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1055 | При передаче несуществующего ID аккаунта модуль завершал свою работу с некорректным текстом ошибки. Также модуль повторно выполнял запрос к API `cloudapi/account/get` без необходимости. | + +### Модуль decort_vins +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1053 | Модуль завершал свою работу ошибкой Python при выполнении восстановления внутренней сети из корзины. | +| BANS-1079 | Модуль отключал внешнюю сеть при незаданном параметре `ext_net_id`. | +| BANS-937 | Модуль завершал работу ошибкой запроса к API при `state: absent` для несуществующего объекта. | + +### Модуль decort_disk +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1062 | Модуль завершал работу ошибкой при `state: absent` для несуществующего объекта. | + +### Модуль decort_k8s +| Идентификатор
задачи | Описание | +| --- | --- | +| BANS-1188 | Модуль завершал работу ошибкой при `state: absent` для объекта, который не найден или безвозвратно удален. | diff --git a/README.md b/README.md index 1100576..5994f8e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ | Версия платформы | Версия модулей Ansible | |:----------------:|:--------------------------:| +| 4.6.0 | 12.0.x | | 4.5.0 | 11.0.x | | 4.4.0 | 10.0.x | | 4.4.0 build 963 | 9.0.x | diff --git a/library/decort_account.py b/library/decort_account.py index c7d143f..2dc31f4 100644 --- a/library/decort_account.py +++ b/library/decort_account.py @@ -7,9 +7,9 @@ module: decort_account description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 ''' -from typing import Iterable +from typing import Any, Iterable from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.decort_utils import DecortController +from ansible.module_utils.decort_utils import DecortController, sdk_types class DecortAccount(DecortController): @@ -23,7 +23,7 @@ class DecortAccount(DecortController): def amodule_init_args(self) -> dict: return self.pack_amodule_init_args( argument_spec=dict( - access_emails=dict( + send_access_emails=dict( type='bool', ), acl=dict( @@ -45,8 +45,13 @@ class DecortAccount(DecortController): options=dict( rights=dict( type='str', - choices=['R', 'RCX', 'ARCXDU'], - default='R', + choices=( + sdk_types.AccessTypeForSet + ._member_names_ + ), + default=( + sdk_types.AccessTypeForSet.R.name + ), ), id=dict( type='str', @@ -69,19 +74,19 @@ class DecortAccount(DecortController): quotas=dict( type='dict', options=dict( - cpu=dict( + cpu_count=dict( type='int', ), - disks_size=dict( + storage_size_gb=dict( type='int', ), - gpu=dict( + gpu_count=dict( type='int', ), - public_ip=dict( + ext_ip_count=dict( type='int', ), - ram=dict( + ram_size_mb=dict( type='int', ), ), @@ -135,7 +140,7 @@ class DecortAccount(DecortController): # Parameters or combinations of parameters that can # cause changing the object. changing_params = [ - 'access_emails', + 'send_access_emails', 'acl', ['id', 'name'], 'quotas', @@ -188,110 +193,308 @@ class DecortAccount(DecortController): self.acc_id, self._acc_info = self.account_find( account_name=self.aparams['name'], account_id=self.aparams['id'], - resource_consumption=self.aparams['get_resource_consumption'], - ) + ) # If this is a repeated getting info else: # If check mode is enabled, there is no needed to # request info again if not self.amodule.check_mode: self.acc_id, self._acc_info = self.account_find( - account_id=self.acc_id, - resource_consumption=( - self.aparams['get_resource_consumption'] - ), + account_id=self._acc_info.id, ) - self.facts = self.acc_info + + if self._acc_info is not None: + acc_info_dict = self.acc_info.model_dump() + if self.aparams['get_resource_consumption']: + resource_consumption_dict = ( + self.api.cloudapi.account.get_resource_consumption( + account_id=self.acc_info.id, + ).model_dump() + ) + resource_consumption_dict.pop('id') + resource_consumption_dict.pop('quotas') + acc_info_dict['resource_consumption'] = ( + resource_consumption_dict + ) + self.facts = acc_info_dict def change(self): self.change_state() self.change_acl() - if self.account_update_args: - self.account_update(account_id=self.acc_id, - **self.account_update_args) + need_account_update_api_call: bool = False + + sdk_param_send_access_emails = None + aparam_send_access_emails: bool | None = self.aparams[ + 'send_access_emails' + ] + if ( + aparam_send_access_emails is not None + and self.acc_info.send_access_emails != aparam_send_access_emails + ): + sdk_param_send_access_emails = aparam_send_access_emails + need_account_update_api_call = True + + sdk_param_name = None + aparam_name: str | None = self.aparams['name'] + if ( + self.aparams['id'] and aparam_name + and self.acc_info.name != aparam_name + ): + sdk_param_name = aparam_name + need_account_update_api_call = True + + sdk_param_cpu_count_quota = None + sdk_param_storage_size_quota_gb = None + sdk_param_gpu_count_quota = None + sdk_param_ext_ip_count_quota = None + sdk_param_ram_size_quota_mb = None + aparam_quotas: dict[str, Any] = self.aparams['quotas'] + if aparam_quotas: + aparam_quotas_cpu_count: int | None = aparam_quotas['cpu_count'] + if ( + aparam_quotas_cpu_count is not None + and self.acc_info.quotas.cpu_count != aparam_quotas_cpu_count + ): + sdk_param_cpu_count_quota = aparam_quotas_cpu_count + need_account_update_api_call = True + + aparam_quotas_storage_size_gb: int | None = aparam_quotas[ + 'storage_size_gb' + ] + if ( + aparam_quotas_storage_size_gb is not None + and ( + self.acc_info.quotas.storage_size_gb + != aparam_quotas_storage_size_gb + ) + ): + sdk_param_storage_size_quota_gb = aparam_quotas_storage_size_gb + need_account_update_api_call = True + + aparam_quotas_gpu_count: int | None = aparam_quotas['gpu_count'] + if ( + aparam_quotas_gpu_count is not None + and self.acc_info.quotas.gpu_count != aparam_quotas_gpu_count + ): + sdk_param_gpu_count_quota = aparam_quotas_gpu_count + need_account_update_api_call = True + + aparam_quotas_ext_ip_count: int | None = aparam_quotas[ + 'ext_ip_count' + ] + if ( + aparam_quotas_ext_ip_count is not None + and ( + self.acc_info.quotas.ext_ip_count + != aparam_quotas_ext_ip_count + ) + ): + sdk_param_ext_ip_count_quota = aparam_quotas_ext_ip_count + need_account_update_api_call = True + + aparam_quotas_ram_size_mb: int | None = aparam_quotas[ + 'ram_size_mb' + ] + if ( + aparam_quotas_ram_size_mb is not None + and ( + self.acc_info.quotas.ram_size_mb + != aparam_quotas_ram_size_mb + ) + ): + sdk_param_ram_size_quota_mb = aparam_quotas_ram_size_mb + need_account_update_api_call = True + + sdk_param_sep_pools = None + aparam_sep_pools: list[dict[str, Any]] = self.aparams['sep_pools'] + if aparam_sep_pools is not None: + sep_pools: set[str] = set() + for sep in aparam_sep_pools: + sep_pool_names: list[str] = sep['pool_names'] + for pool_name in sep_pool_names: + sep_pools.add(f'{sep["sep_id"]}_{pool_name}') + if set(self.acc_info.sep_pools) != sep_pools: + sdk_param_sep_pools = list(sep_pools) + need_account_update_api_call = True + + sdk_param_description = None + aparam_desc: str | None = self.aparams['description'] + if ( + aparam_desc is not None + and self.acc_info.description != aparam_desc + ): + sdk_param_description = aparam_desc + need_account_update_api_call = True + + sdk_param_default_zone_id = None + aparam_default_zone_id: int | None = self.aparams['default_zone_id'] + if ( + aparam_default_zone_id is not None + and self.acc_info.default_zone_id != aparam_default_zone_id + ): + sdk_param_default_zone_id = aparam_default_zone_id + need_account_update_api_call = True + + if need_account_update_api_call: + OBJ = 'account' + + self.sdk_checkmode(self.api.ca.account.update)( + account_id=self.acc_info.id, + cpu_count_quota=sdk_param_cpu_count_quota, + gpu_count_quota=sdk_param_gpu_count_quota, + name=sdk_param_name, + ext_ip_count_quota=sdk_param_ext_ip_count_quota, + ram_size_quota_mb=sdk_param_ram_size_quota_mb, + send_access_emails=sdk_param_send_access_emails, + storage_size_quota_gb=sdk_param_storage_size_quota_gb, + sep_pools=sdk_param_sep_pools, + description=sdk_param_description, + default_zone_id=sdk_param_default_zone_id, + ) + + if sdk_param_send_access_emails is not None: + smth = 'sending access emails' + if sdk_param_send_access_emails: + self.message( + self.MESSAGES.obj_smth_enabled( + obj=OBJ, + id=self.acc_info.id, + smth=smth, + ) + ) + else: + self.message( + self.MESSAGES.obj_smth_disabled( + obj=OBJ, + id=self.acc_info.id, + smth=smth, + ) + ) + if sdk_param_name is not None: + self.message( + self.MESSAGES.obj_renamed( + obj=OBJ, + id=self.acc_info.id, + new_name=sdk_param_name, + ) + ) + quotas = { + 'CPU count quota': sdk_param_cpu_count_quota, + 'storage size quota GB': sdk_param_storage_size_quota_gb, + 'GPU count quota': sdk_param_gpu_count_quota, + 'ext IP count quota': sdk_param_ext_ip_count_quota, + 'RAM size quota MB': sdk_param_ram_size_quota_mb, + } + for q_name, q_value in quotas.items(): + if q_value is not None: + self.message( + self.MESSAGES.obj_smth_changed( + obj=OBJ, + id=self.acc_info.id, + smth=q_name, + new_value=q_value + ) + ) + + if sdk_param_default_zone_id is not None: + self.message( + self.MESSAGES.obj_smth_changed( + obj=OBJ, + id=self.acc_info.id, + smth='default_zone_id', + new_value=sdk_param_default_zone_id, + ) + ) + self.get_info() def change_state(self): - match self._acc_info: - case None: - self.message(self.MESSAGES.obj_not_found(obj=self.OBJ)) - match self.aparams: - case {'state': 'absent' | 'absent_permanently'}: - pass - case {'state': 'confirmed' | 'disabled' | 'present'}: - self.exit(fail=True) - case {'status': 'DESTROYED'}: - match self.aparams: - case {'state': 'absent' | 'absent_permanently'}: - self.message( - self.MESSAGES.obj_deleted( - obj=self.OBJ, - id=self.acc_id, - permanently=True, - already=True, - ) + if self._acc_info is None: + self.message(self.MESSAGES.obj_not_found(obj=self.OBJ)) + match self.aparams: + case {'state': 'absent' | 'absent_permanently'}: + pass + case {'state': 'confirmed' | 'disabled' | 'present'}: + self.exit(fail=True) + elif self._acc_info.status == sdk_types.AccountStatus.DESTROYED: + match self.aparams: + case {'state': 'absent' | 'absent_permanently'}: + self.message( + self.MESSAGES.obj_deleted( + obj=self.OBJ, + id=self.acc_info.id, + permanently=True, + already=True, ) - case {'state': 'confirmed' | 'disabled' | 'present'}: - self.message( - self.MESSAGES.obj_not_restored(obj=self.OBJ, - id=self.acc_id) - ) - self.exit(fail=True) - case {'status': 'DELETED'}: - match self.aparams: - case {'state': 'absent'}: - self.message( - self.MESSAGES.obj_deleted( - obj=self.OBJ, - id=self.acc_id, - permanently=False, - already=True, - ) + ) + case {'state': 'confirmed' | 'disabled' | 'present'}: + self.message( + self.MESSAGES.obj_not_restored(obj=self.OBJ, + id=self.acc_info.id) ) - case {'state': 'absent_permanently'}: - self.delete(permanently=True) - case {'state': 'confirmed' | 'present'}: - self.restore() - case {'state': 'disabled'}: - self.restore() - self.disable() - case {'status': 'CONFIRMED'}: - match self.aparams: - case {'state': 'absent'}: - self.delete() - case {'state': 'absent_permanently'}: - self.delete(permanently=True) - case {'state': 'confirmed' | 'present'}: - pass - case {'state': 'disabled'}: - self.disable() - case {'status': 'DISABLED'}: - match self.aparams: - case {'state': 'absent'}: - self.delete() - case {'state': 'absent_permanently'}: - self.delete(permanently=True) - case {'state': 'confirmed'}: - self.enable() - case {'state': 'present' | 'disabled'}: - pass + self.exit(fail=True) + elif self._acc_info.status == sdk_types.AccountStatus.DELETED: + match self.aparams: + case {'state': 'absent'}: + self.message( + self.MESSAGES.obj_deleted( + obj=self.OBJ, + id=self.acc_info.id, + permanently=False, + already=True, + ) + ) + case {'state': 'absent_permanently'}: + self.delete(permanently=True) + case {'state': 'confirmed' | 'present'}: + self.restore() + case {'state': 'disabled'}: + self.restore() + self.disable() + elif self._acc_info.status == sdk_types.AccountStatus.CONFIRMED: + match self.aparams: + case {'state': 'absent'}: + self.delete() + case {'state': 'absent_permanently'}: + self.delete(permanently=True) + case {'state': 'confirmed' | 'present'}: + pass + case {'state': 'disabled'}: + self.disable() + elif self._acc_info.status == sdk_types.AccountStatus.DISABLED: + match self.aparams: + case {'state': 'absent'}: + self.delete() + case {'state': 'absent_permanently'}: + self.delete(permanently=True) + case {'state': 'confirmed'}: + self.enable() + case {'state': 'present' | 'disabled'}: + pass def delete(self, permanently=False): - self.account_delete(account_id=self.acc_id, permanently=permanently) + self.account_delete( + account_id=self.acc_info.id, + permanently=permanently + ) self.get_info() def disable(self): - self.account_disable(account_id=self.acc_id) + self.sdk_checkmode(self.api.ca.account.disable)( + account_id=self.acc_info.id + ) self.get_info() def enable(self): - self.account_enable(account_id=self.acc_id) + self.sdk_checkmode(self.api.ca.account.enable)( + account_id=self.acc_info.id + ) self.get_info() def restore(self): - self.account_restore(account_id=self.acc_id) + self.account_restore(account_id=self.acc_info.id) self.get_info() def change_acl(self): @@ -299,7 +502,7 @@ class DecortAccount(DecortController): return actual_users = { - u['userGroupId']: u['right'] for u in self.acc_info['acl'] + u.user_name: u.access_type for u in self.acc_info.acl } actual_users_ids = set(actual_users.keys()) @@ -328,8 +531,8 @@ class DecortAccount(DecortController): aparams_users_ids.intersection(actual_users_ids) upd_users = dict() for id in upd_users_ids: - if actual_users[id] == 'CXDRAU': - actual_user_rights = 'ARCXDU' + if actual_users[id] == sdk_types.AccessType.CXDRAU: + actual_user_rights = sdk_types.AccessTypeForSet.ARCXDU else: actual_user_rights = actual_users[id] @@ -341,68 +544,12 @@ class DecortAccount(DecortController): actual_users_ids.difference(aparams_users_ids) if del_users_ids or new_users or upd_users: - self.account_change_acl(account_id=self.acc_id, + self.account_change_acl(account_id=self.acc_info.id, del_users=del_users_ids, add_users=new_users, upd_users=upd_users) self.get_info() - @property - def account_update_args(self) -> dict: - result_args = dict() - - aparam_access_emails = self.aparams['access_emails'] - if (aparam_access_emails is not None - and self.acc_info['sendAccessEmails'] != aparam_access_emails): - result_args['access_emails'] = aparam_access_emails - - aparam_name = self.aparams['name'] - if (self.aparams['id'] and aparam_name - and self.acc_info['name'] != aparam_name): - result_args['name'] = aparam_name - - aparam_quotas = self.aparams['quotas'] - if aparam_quotas: - quotas_naming = [ - ['cpu', 'CU_C', 'cpu_quota'], - ['disks_size', 'CU_DM', 'disks_size_quota'], - ['gpu', 'gpu_units', 'gpu_quota'], - ['public_ip', 'CU_I', 'public_ip_quota'], - ['ram', 'CU_M', 'ram_quota'], - ] - for aparam, info_key, result_arg in quotas_naming: - current_value = int(self.acc_info['resourceLimits'][info_key]) - if (aparam_quotas[aparam] is not None - and current_value != aparam_quotas[aparam]): - result_args[result_arg] = aparam_quotas[aparam] - - aparam_sep_pools = self.aparams['sep_pools'] - if aparam_sep_pools is not None: - sep_pools = set() - for sep in aparam_sep_pools: - for pool_name in sep['pool_names']: - sep_pools.add( - f'{sep["sep_id"]}_{pool_name}' - ) - if set(self.acc_info['uniqPools']) != sep_pools: - result_args['sep_pools'] = sep_pools - - aparam_desc = self.aparams['description'] - if ( - aparam_desc is not None - and self.acc_info['description'] != aparam_desc - ): - result_args['description'] = aparam_desc - - aparam_default_zone_id = self.aparams['default_zone_id'] - if ( - aparam_default_zone_id is not None - and self.acc_info['defaultZoneId'] != aparam_default_zone_id - ): - result_args['default_zone_id'] = aparam_default_zone_id - - return result_args - def check_aparam_default_zone_id(self) -> bool | None: aparam_default_zone_id = self.aparams['default_zone_id'] if aparam_default_zone_id is not None: diff --git a/library/decort_bservice.py b/library/decort_bservice.py index 34703ab..10402db 100644 --- a/library/decort_bservice.py +++ b/library/decort_bservice.py @@ -39,21 +39,21 @@ class decort_bservice(DecortController): self.fail_json(**self.result) # fail the module -> exit # now validate RG - validated_rg_id, validated_rg_facts = self.rg_find( + validated_rg_id, validated_rg_model = self.rg_find( arg_account_id=validated_acc_id, arg_rg_id=arg_amodule.params['rg_id'], arg_rg_name=arg_amodule.params['rg_name'] ) - if not validated_rg_id: + if not validated_rg_id or not validated_rg_model: self.result['failed'] = True self.result['changed'] = False self.result['msg'] = "Cannot find RG ID {} / name '{}'.".format(arg_amodule.params['rg_id'], arg_amodule.params['rg_name']) - self.fail_json(**self.result) + self.amodule.fail_json(**self.result) arg_amodule.params['rg_id'] = validated_rg_id - arg_amodule.params['rg_name'] = validated_rg_facts['name'] - validated_acc_id = validated_rg_facts['accountId'] + arg_amodule.params['rg_name'] = validated_rg_model.name + validated_acc_id = validated_rg_model.account_id self.bservice_id, self.bservice_info = self.bservice_find( validated_acc_id, @@ -104,11 +104,11 @@ class decort_bservice(DecortController): return def create(self): - self.bservice_id = self.bservice_id = self.bservice_provision( - self.amodule.params['name'], - self.amodule.params['rg_id'], - self.amodule.params['sshuser'], - self.amodule.params['sshkey'], + self.bservice_id = self.sdk_checkmode(self.api.ca.bservice.create)( + name=self.amodule.params['name'], + rg_id=self.amodule.params['rg_id'], + ssh_user_name=self.amodule.params['sshuser'], + ssh_public_key=self.amodule.params['sshkey'], zone_id=self.aparams['zone_id'], ) if self.bservice_id: @@ -136,7 +136,10 @@ class decort_bservice(DecortController): pass def destroy(self): - self.bservice_delete(self.bservice_id) + self.sdk_checkmode(self.api.ca.bservice.delete)( + bservice_id=self.bservice_id, + permanently=True, + ) self.bservice_info['status'] = 'DELETED' self.bservice_should_exist = False return diff --git a/library/decort_disk.py b/library/decort_disk.py index 11cf023..8305f44 100644 --- a/library/decort_disk.py +++ b/library/decort_disk.py @@ -58,6 +58,7 @@ class decort_disk(DecortController): name=arg_amodule.params['name'] if "name" in arg_amodule.params else "", account_id=self.acc_id, check_state=False, + fail_if_not_found=False, ) if arg_amodule.params['place_with']: @@ -67,18 +68,30 @@ class decort_disk(DecortController): self.disk_id = validated_disk_id self.disk_info = validated_disk_facts - if self.disk_id: - self.acc_id = validated_disk_facts['accountId'] + if self.disk_id and self.disk_info.status not in [ + sdk_types.DiskStatus.DESTROYED, + sdk_types.DiskStatus.PURGED, + ]: + self.acc_id = validated_disk_facts.account_id self.check_amodule_args_for_change() - else: + elif ( + self.amodule.params['state'] == 'present' + and not arg_amodule.params['id'] + ): self.check_amodule_args_for_create() - def compare_iotune_params(self, new_iotune: dict, current_iotune: dict): + def compare_iotune_params( + self, + new_iotune: dict, + current_iotune: sdk_types.IOTuneAPIResultNM, + ): io_fields = sdk_types.IOTuneAPIResultNM.model_fields.keys() for field in io_fields: new_value = new_iotune.get(field) - current_value = current_iotune.get(field) + if new_value is None: + continue + current_value = getattr(current_iotune, field, None) if new_value != current_value: return False @@ -115,7 +128,11 @@ class decort_disk(DecortController): ) #IO tune aparam_limit_io: dict[str, int | None] = self.amodule.params['limitIO'] - self.limit_io(aparam_limit_io=aparam_limit_io) + if ( + aparam_limit_io + and any(value is not None for value in aparam_limit_io.values()) + ): + self.limit_io(aparam_limit_io=aparam_limit_io) #set share status if self.amodule.params['shareable']: self.sdk_checkmode(self.api.cloudapi.disks.share)( @@ -133,13 +150,13 @@ class decort_disk(DecortController): #rename if id present if ( self.amodule.params['name'] is not None - and self.amodule.params['name'] != self.disk_info['name'] + and self.amodule.params['name'] != self.disk_info.name ): self.rename() #resize if ( self.amodule.params['size'] is not None - and self.amodule.params['size'] != self.disk_info['sizeMax'] + and self.amodule.params['size'] != self.disk_info.size_max_gb ): self.sdk_checkmode(self.api.cloudapi.disks.resize2)( disk_id=self.disk_id, @@ -147,16 +164,19 @@ class decort_disk(DecortController): ) #IO TUNE aparam_limit_io: dict[str, int | None] = self.amodule.params['limitIO'] - if aparam_limit_io: + if ( + aparam_limit_io + and any(value is not None for value in aparam_limit_io.values()) + ): if not self.compare_iotune_params( new_iotune=aparam_limit_io, - current_iotune=self.disk_info['iotune'], + current_iotune=self.disk_info.io_tune, ): self.limit_io(aparam_limit_io=aparam_limit_io) #share check/update #raise Exception(self.amodule.params['shareable']) - if self.amodule.params['shareable'] != self.disk_info['shareable']: + if self.amodule.params['shareable'] != self.disk_info.shared: if self.amodule.params['shareable']: self.sdk_checkmode(self.api.cloudapi.disks.share)( disk_id=self.disk_id, @@ -169,7 +189,7 @@ class decort_disk(DecortController): aparam_storage_policy_id = self.aparams['storage_policy_id'] if ( aparam_storage_policy_id is not None - and aparam_storage_policy_id != self.disk_info['storage_policy_id'] + and aparam_storage_policy_id != self.disk_info.storage_policy_id ): self.sdk_checkmode(self.api.ca.disks.change_disk_storage_policy)( disk_id=self.disk_id, @@ -183,7 +203,7 @@ class decort_disk(DecortController): detach=self.amodule.params['force_detach'], permanently=self.amodule.params['permanently'], ) - self.disk_id, self.disk_info = self._disk_get_by_id(self.disk_id) + self.disk_info = self._disk_get_by_id(disk_id=self.disk_id) return def rename(self): @@ -199,7 +219,7 @@ class decort_disk(DecortController): self.result['changed'] = False if self.disk_id: self.result['msg'] = ("No state change required for Disk ID {} because of its " - "current status '{}'.").format(self.disk_id, self.disk_info['status']) + "current status '{}'.").format(self.disk_id, self.disk_info.status) else: self.result['msg'] = ("No state change to '{}' can be done for " "non-existent Disk.").format(self.amodule.params['state']) @@ -219,28 +239,7 @@ class decort_disk(DecortController): if check_mode or self.disk_info is None: return ret_dict - # remove io param with zero value - clean_io = [param for param in self.disk_info['iotune'] if self.disk_info['iotune'][param] == 0] - for key in clean_io: del self.disk_info['iotune'][key] - - ret_dict['id'] = self.disk_info['id'] - ret_dict['name'] = self.disk_info['name'] - ret_dict['size'] = self.disk_info['sizeMax'] - ret_dict['state'] = self.disk_info['status'] - ret_dict['account_id'] = self.disk_info['accountId'] - ret_dict['sep_id'] = self.disk_info['sepId'] - ret_dict['pool'] = self.disk_info['pool'] - ret_dict['computes'] = self.disk_info['computes'] - ret_dict['gid'] = self.disk_info['gid'] - ret_dict['iotune'] = self.disk_info['iotune'] - ret_dict['size_available'] = self.disk_info['sizeAvailable'] - ret_dict['size_used'] = self.disk_info['sizeUsed'] - ret_dict['storage_policy_id'] = self.disk_info['storage_policy_id'] - ret_dict['to_clean'] = self.disk_info['to_clean'] - ret_dict['cache_mode'] = self.disk_info['cache'] - ret_dict['blkdiscard'] = self.disk_info['blkdiscard'] - - return ret_dict + return self.disk_info.model_dump() @property def amodule_init_args(self) -> dict: @@ -391,8 +390,8 @@ class decort_disk(DecortController): aparam_storage_policy_id = self.aparams['storage_policy_id'] if ( aparam_storage_policy_id is not None - and aparam_storage_policy_id - not in self.acc_info['storage_policy_ids'] + and aparam_storage_policy_id + not in self.acc_info.storage_policy_ids ): check_errors = True self.message( @@ -431,24 +430,24 @@ class decort_disk(DecortController): amodule = self.amodule if self.disk_id: #disk exist - if self.disk_info['status'] in ["MODELED", "CREATING"]: + if self.disk_info.status in [sdk_types.DiskStatus.MODELED, sdk_types.DiskStatus.CREATING]: self.result['failed'] = True self.result['changed'] = False self.result['msg'] = ("No change can be done for existing Disk ID {} because of its current " - "status '{}'").format(self.disk_id, self.disk_info['status']) + "status '{}'").format(self.disk_id, self.disk_info.status) # "ASSIGNED","CREATED","DELETED","PURGED", "DESTROYED" - elif self.disk_info['status'] in ["ASSIGNED","CREATED"]: + elif self.disk_info.status in [sdk_types.DiskStatus.CREATED, sdk_types.DiskStatus.ASSIGNED]: if amodule.params['state'] == 'absent': self.delete() elif amodule.params['state'] == 'present': self.action() - elif self.disk_info['status'] in ["PURGED", "DESTROYED"]: + elif self.disk_info.status in [sdk_types.DiskStatus.PURGED, sdk_types.DiskStatus.DESTROYED]: #re-provision disk if amodule.params['state'] in ('present'): self.create() else: self.nop() - elif self.disk_info['status'] == "DELETED": + elif self.disk_info.status == sdk_types.DiskStatus.DELETED: if amodule.params['state'] in ('present'): self.action(restore=True) elif (amodule.params['state'] == 'absent' and @@ -459,8 +458,24 @@ class decort_disk(DecortController): else: # preexisting Disk was not found if amodule.params['state'] == 'absent': - self.nop() - else: + self.exit() + + if ( + ( + amodule.params['state'] == 'present' + or amodule.params['state'] is None + ) + and amodule.params['id'] + ): + self.message( + f'Disk with ID {amodule.params['id']} not found.' + ) + self.exit(fail=True) + + if ( + amodule.params['state'] == 'present' + and not amodule.params['id'] + ): self.create() if self.result['failed']: diff --git a/library/decort_disk_list.py b/library/decort_disk_list.py index bb194bc..1ccb0f9 100644 --- a/library/decort_disk_list.py +++ b/library/decort_disk_list.py @@ -59,9 +59,11 @@ class DecortDiskList(DecortController): storage_policy_id=dict( type='int', ), - type=dict( - type='str', - choices=sdk_types.DiskType._member_names_, + rg_id=dict( + type='int', + ), + vm_id=dict( + type='int', ), ), ), @@ -108,7 +110,6 @@ class DecortDiskList(DecortController): def get_info(self): aparam_filter: dict[str, Any] = self.aparams['filter'] aparam_status: str | None = aparam_filter['status'] - aparam_type: str | None = aparam_filter['type'] aparam_pagination: dict[str, Any] = self.aparams['pagination'] @@ -137,10 +138,8 @@ class DecortDiskList(DecortController): if aparam_status else None ), storage_policy_id=aparam_filter['storage_policy_id'], - type=( - sdk_types.DiskType[aparam_type] - if aparam_type else None - ), + rg_id=aparam_filter['rg_id'], + vm_id=aparam_filter['vm_id'], page_number=aparam_pagination['number'], page_size=aparam_pagination['size'], sort_by=sort_by, diff --git a/library/decort_group.py b/library/decort_group.py index d92debd..edb24a1 100644 --- a/library/decort_group.py +++ b/library/decort_group.py @@ -356,7 +356,7 @@ class decort_group(DecortController): else: if ( aparam_storage_policy_id - not in self.rg_info['storage_policy_ids'] + not in self.rg_info.storage_policy_ids ): check_errors = True self.message( @@ -366,7 +366,7 @@ class decort_group(DecortController): ) if ( aparam_storage_policy_id - not in self.acc_info['storage_policy_ids'] + not in self.acc_info.storage_policy_ids ): check_errors = True self.message( diff --git a/library/decort_image.py b/library/decort_image.py index f8b8a18..7d45177 100644 --- a/library/decort_image.py +++ b/library/decort_image.py @@ -18,8 +18,8 @@ class decort_image(DecortController): super(decort_image, self).__init__(AnsibleModule(**self.amodule_init_args)) amodule = self.amodule - self.validated_image_id = 0 - self.validated_virt_image_id = 0 + self.validated_image_id: int = 0 + self.validated_virt_image_id: int = 0 self.validated_image_name = amodule.params['image_name'] self.validated_virt_image_name = None self.image_info: dict @@ -180,7 +180,9 @@ class decort_image(DecortController): def decort_image_delete(self,amodule): # function that removes an image - self.image_delete(imageId=amodule.image_id_delete) + self.sdk_checkmode(self.api.ca.image.delete)( + image_id=amodule.image_id_delete + ) _, image_facts = decort_image._image_get_by_id(self, amodule.image_id_delete) self.result['facts'] = decort_image.decort_image_package_facts(image_facts, amodule.check_mode) return @@ -195,17 +197,24 @@ class decort_image(DecortController): image_id, image_facts = decort_image.decort_virt_image_find(self, amodule) self.result['facts'] = decort_image.decort_image_package_facts(image_facts, amodule.check_mode) return image_id, image_facts - + + @DecortController.handle_sdk_exceptions def decort_image_rename(self,amodule): # image renaming function - image_facts = self.image_rename(imageId=self.validated_image_id, name=amodule.params['image_name']) + self.sdk_checkmode(self.api.ca.image.rename)( + image_id=self.validated_image_id, + name=amodule.params['image_name'], + ) self.result['msg'] = ("Image renamed successfully") image_id, image_facts = decort_image.decort_image_find(self, amodule) return image_id, image_facts - + + @DecortController.handle_sdk_exceptions def decort_virt_image_rename(self, amodule): - image_facts = self.image_rename(imageId=self.validated_virt_image_id, - name=amodule.params['virt_name']) + self.sdk_checkmode(self.api.ca.image.rename)( + image_id=self.validated_virt_image_id, + name=amodule.params['virt_name'], + ) self.result['msg'] = ("Virtual image renamed successfully") image_id, image_facts = self.decort_virt_image_find(amodule) return image_id, image_facts @@ -264,6 +273,8 @@ class decort_image(DecortController): ret_dict['hot_resize'] = arg_image_facts['hotResize'] ret_dict['storage_policy_id'] = arg_image_facts['storage_policy_id'] ret_dict['to_clean'] = arg_image_facts['to_clean'] + ret_dict['independent'] = arg_image_facts['independent'] + ret_dict['links_to'] = arg_image_facts['linksTo'] return ret_dict @property @@ -372,7 +383,7 @@ class decort_image(DecortController): if ( aparam_storage_policy_id is not None and aparam_storage_policy_id - not in self.acc_info['storage_policy_ids'] + not in self.acc_info.storage_policy_ids ): check_errors = True self.message( @@ -426,7 +437,7 @@ class decort_image(DecortController): ) elif ( aparam_storage_policy_id - not in self.acc_info['storage_policy_ids'] + not in self.acc_info.storage_policy_ids ): check_errors = True self.message( diff --git a/library/decort_k8s.py b/library/decort_k8s.py index 0d525e4..37ad35e 100644 --- a/library/decort_k8s.py +++ b/library/decort_k8s.py @@ -20,7 +20,7 @@ class decort_k8s(DecortController): validated_acc_id = 0 validated_rg_id = 0 - validated_rg_facts = None + validated_rg_model = None validated_k8ci_id = 0 self.k8s_should_exist = False self.is_k8s_stopped_or_will_be_stopped: None | bool = None @@ -55,12 +55,12 @@ class decort_k8s(DecortController): self.amodule.fail_json(**self.result) # fail the module -> exit # now validate RG - validated_rg_id, validated_rg_facts = self.rg_find( + validated_rg_id, validated_rg_model = self.rg_find( arg_account_id=validated_acc_id, arg_rg_id=arg_amodule.params['rg_id'], arg_rg_name=arg_amodule.params['rg_name'] ) - if not validated_rg_id: + if not validated_rg_id or not validated_rg_model: self.result['failed'] = True self.result['changed'] = False self.result['msg'] = "Cannot find RG ID {} / name '{}'.".format(arg_amodule.params['rg_id'], @@ -70,19 +70,21 @@ class decort_k8s(DecortController): self.rg_id = validated_rg_id arg_amodule.params['rg_id'] = validated_rg_id - arg_amodule.params['rg_name'] = validated_rg_facts['name'] - self.acc_id = validated_rg_facts['accountId'] + arg_amodule.params['rg_name'] = validated_rg_model.name + self.acc_id = validated_rg_model.account_id - self.k8s_id,self.k8s_info = self.k8s_find(k8s_id=arg_amodule.params['id'], - k8s_name=arg_amodule.params['name'], - rg_id=validated_rg_id, - check_state=False) + self.k8s_id, self._k8s_info = self.k8s_find( + k8s_id=arg_amodule.params['id'], + k8s_name=arg_amodule.params['name'], + rg_id=validated_rg_id, + check_state=False, + ) if self.k8s_id and self.k8s_info['status'] != 'DESTROYED': self.k8s_should_exist = True - self.acc_id = self.k8s_info['accountId'] + self.acc_id = self.k8s_info['account_id'] self.check_amodule_args_for_change() - else: + elif arg_amodule.params['state'] != 'absent': self.check_amodule_args_for_create() return @@ -96,32 +98,14 @@ class decort_k8s(DecortController): config=None, ) - if self.amodule.params['getConfig'] and self.k8s_info['techStatus'] == "STARTED": - ret_dict['config'] = self.k8s_getConfig() - if check_mode: - # in check mode return immediately with the default values return ret_dict - #if self.k8s_facts is None: - # #if void facts provided - change state value to ABSENT and return - # ret_dict['state'] = "ABSENT" - # return ret_dict - - ret_dict['id'] = self.k8s_info['id'] - ret_dict['name'] = self.k8s_info['name'] - ret_dict['techStatus'] = self.k8s_info['techStatus'] - ret_dict['state'] = self.k8s_info['status'] - ret_dict['rg_id'] = self.k8s_info['rgId'] - ret_dict['vins_id'] = self.k8s_vins_id - ret_dict['account_id'] = self.acc_id - ret_dict['k8s_Masters'] = self.k8s_info['k8sGroups']['masters'] - ret_dict['k8s_Workers'] = self.k8s_info['k8sGroups']['workers'] - ret_dict['lb_id'] = self.k8s_info['lbId'] - ret_dict['description'] = self.k8s_info['desc'] - ret_dict['zone_id'] = self.k8s_info['zoneId'] - - return ret_dict + self.k8s_info['vins_id'] = self.k8s_vins_id + self.k8s_info['config'] = None + if self.amodule.params['getConfig'] and self.k8s_info['tech_status'] == "STARTED": + self.k8s_info['config'] = self.k8s_getConfig() + return self.k8s_info def nop(self): """No operation (NOP) handler for k8s cluster management by decort_k8s module. @@ -204,10 +188,12 @@ class decort_k8s(DecortController): self.result['failed'] = True self.amodule.fail_json(**self.result) - self.k8s_id,self.k8s_info = self.k8s_find(k8s_id=k8s_id, - k8s_name=self.amodule.params['name'], - rg_id=self.rg_id, - check_state=False) + self.k8s_id, self._k8s_info = self.k8s_find( + k8s_id=k8s_id, + k8s_name=self.amodule.params['name'], + rg_id=self.rg_id, + check_state=False, + ) if self.k8s_id: self.k8s_should_exist = True @@ -219,19 +205,22 @@ class decort_k8s(DecortController): self.aparams['storage_policy_id'] ), ) - self.k8s_info = self.k8s_get_by_id(k8s_id=self.k8s_id) + self._k8s_info = self.k8s_get_by_id(k8s_id=self.k8s_id) return def destroy(self): - self.k8s_delete(self.k8s_id,self.amodule.params['permanent']) + self.sdk_checkmode(self.api.ca.k8s.delete)( + k8s_id=self.k8s_id, + permanently=self.amodule.params['permanent'], + ) self.k8s_info['status'] = 'DELETED' self.k8s_should_exist = False return def action(self, disared_state, preupdate: bool = False): if self.amodule.params['master_chipset'] is not None: - for master_node in self.k8s_info['k8sGroups']['masters'][ - 'detailedInfo' + for master_node in self.k8s_info['node_groups']['master'][ + 'vms' ]: _, master_node_info, _ = self._compute_get_by_id( comp_id=master_node['id'] @@ -247,17 +236,27 @@ class decort_k8s(DecortController): self.exit(fail=True) if ( - self.aparams['name'] is not None - and self.aparams['name'] != self.k8s_info['name'] + ( + self.aparams['name'] is not None + and self.aparams['name'] != self.k8s_info['name'] + ) + or ( + self.aparams['description'] is not None + and self.aparams['description'] != self.k8s_info['description'] + ) ): - self.k8s_update(id=self.k8s_id, name=self.aparams['name']) + self.sdk_checkmode(self.api.ca.k8s.update)( + k8s_id=self.k8s_id, + description=self.aparams['description'], + name=self.aparams['name'], + ) if preupdate: # K8s info updating - self.k8s_info = self.k8s_get_by_id(k8s_id=self.k8s_id) + self._k8s_info = self.k8s_get_by_id(k8s_id=self.k8s_id) #k8s state self.k8s_state(self.k8s_info, disared_state) - self.k8s_info = self.k8s_get_by_id(k8s_id=self.k8s_id) + self._k8s_info = self.k8s_get_by_id(k8s_id=self.k8s_id) #check groups and modify if needed if self.aparams['workers'] is not None: self.k8s_workers_modify( @@ -266,14 +265,17 @@ class decort_k8s(DecortController): ) aparam_zone_id = self.aparams['zone_id'] - if aparam_zone_id is not None and aparam_zone_id != self.k8s_info['zoneId']: + if ( + aparam_zone_id is not None + and aparam_zone_id != self.k8s_info['zone_id'] + ): self.k8s_migrate_to_zone( k8s_id=self.k8s_id, zone_id=aparam_zone_id, ) if self.result['changed'] == True: - self.k8s_info = self.k8s_get_by_id(k8s_id=self.k8s_id) + self._k8s_info = self.k8s_get_by_id(k8s_id=self.k8s_id) #TODO check workers metadata and modify if needed return @@ -413,7 +415,6 @@ class decort_k8s(DecortController): ), description=dict( type='str', - default='Created by decort ansible module', ), with_lb=dict( type='bool', @@ -473,25 +474,25 @@ class decort_k8s(DecortController): self.is_k8s_stopped_or_will_be_stopped = ( ( - self.k8s_info['techStatus'] == 'STOPPED' + self.k8s_info['tech_status'] == 'STOPPED' and ( self.aparams['state'] is None or self.aparams['state'] in ('present', 'stopped') ) ) or ( - self.k8s_info['techStatus'] != 'STOPPED' + self.k8s_info['tech_status'] != 'STOPPED' and self.aparams['state'] == 'stopped' ) ) aparam_sysctl = self.aparams['lb_sysctl'] if aparam_sysctl is not None: - _, lb_info = self._lb_get_by_id(lb_id=self.k8s_info['lbId']) + lb_model = self._lb_get_by_id(lb_id=self.k8s_info['lb_id']) sysctl_with_str_values = { k: str(v) for k, v in aparam_sysctl.items() } - if sysctl_with_str_values != lb_info['sysctlParams']: + if sysctl_with_str_values != lb_model.sysctl_params: self.message( 'Check for parameter "lb_sysctl" failed: ' 'cannot change lb_sysctl for an existing cluster ' @@ -503,7 +504,7 @@ class decort_k8s(DecortController): check_errors = True if ( self.aparams['zone_id'] is not None - and self.aparams['zone_id'] != self.k8s_info['zoneId'] + and self.aparams['zone_id'] != self.k8s_info['zone_id'] and not self.is_k8s_stopped_or_will_be_stopped ): check_errors = True @@ -515,13 +516,11 @@ class decort_k8s(DecortController): aparam_storage_policy_id = self.aparams['storage_policy_id'] if aparam_storage_policy_id is not None: computes_ids = [] - for master_node in self.k8s_info['k8sGroups']['masters'][ - 'detailedInfo' - ]: + for master_node in self.k8s_info['node_groups']['master']['vms']: computes_ids.append(master_node['id']) - for wg in self.k8s_info['k8sGroups']['workers']: + for wg in self.k8s_info['node_groups']['worker']: workers_ids = [ - worker['id'] for worker in wg['detailedInfo'] + worker['id'] for worker in wg['vms'] ] computes_ids.extend(workers_ids) for compute_id in computes_ids: @@ -589,7 +588,7 @@ class decort_k8s(DecortController): ) elif ( aparam_storage_policy_id - not in self.rg_info['storage_policy_ids'] + not in self.rg_info.storage_policy_ids ): check_errors = True self.message( @@ -630,7 +629,9 @@ class decort_k8s(DecortController): if amodule.params['state'] in ( 'disabled', 'enabled', 'present', 'started', 'stopped' ): - self.k8s_restore(self.k8s_id) + self.sdk_checkmode(self.api.ca.k8s.restore)( + k8s_id=self.k8s_id, + ) self.action(disared_state=amodule.params['state'], preupdate=True) if amodule.params['state'] == 'absent': diff --git a/library/decort_lb.py b/library/decort_lb.py index 8befa87..b2bad74 100644 --- a/library/decort_lb.py +++ b/library/decort_lb.py @@ -18,11 +18,9 @@ class decort_lb(DecortController): arg_amodule = self.amodule self.lb_id = 0 - self.lb_facts = None self.vins_id = 0 - self.vins_facts = None self.rg_id = 0 - self.rg_facts = None + self.rg_model = None self.default_server_check = "enabled" self.default_alg = "roundrobin" self.default_settings = { @@ -38,22 +36,34 @@ class decort_lb(DecortController): self.is_lb_stopped_or_will_be_stopped: None | bool = None if arg_amodule.params['lb_id']: - self.lb_id, self.lb_facts = self.lb_find(arg_amodule.params['lb_id']) - if not self.lb_id: - self.result['failed'] = True - self.result['msg'] = "Specified LB ID {} not found."\ - .format(arg_amodule.params['lb_id']) - self.amodule.fail_json(**self.result) - self.rg_id = self.lb_facts['rgId'] - self.vins_id = self.lb_facts['vinsId'] + _, self._lb_info = self.lb_find(lb_id=arg_amodule.params['lb_id']) + if self._lb_info is None: + if arg_amodule.params['state'] == 'absent': + self.nop() + self.exit() + else: + self.message( + self.MESSAGES.obj_not_found( + obj='lb', + id=arg_amodule.params['lb_id'], + ) + ) + self.exit(fail=True) + self.lb_id = self._lb_info.id + self.rg_id = self._lb_info.rg_id + self.vins_id = self._lb_info.vins_id elif arg_amodule.params['rg_id']: - self.rg_id, self.rg_facts = self.rg_find(0,arg_amodule.params['rg_id'], arg_rg_name="") - if not self.rg_id: + self.rg_id, self.rg_model = self.rg_find( + 0, + arg_amodule.params['rg_id'], + arg_rg_name="", + ) + if not self.rg_id or not self.rg_model: self.result['failed'] = True self.result['msg'] = "Specified RG ID {} not found.".format(arg_amodule.params['rg_id']) self.amodule.fail_json(**self.result) - self.acc_id = self.rg_facts['accountId'] + self.acc_id = self.rg_model.account_id elif arg_amodule.params['account_id'] or arg_amodule.params['account_name'] != "": if not arg_amodule.params['rg_name']: @@ -67,24 +77,22 @@ class decort_lb(DecortController): self.result['msg'] = ("Current user does not have access to the requested account " "or non-existent account specified.") self.amodule.fail_json(**self.result) - self.rg_id, self.rg_facts = self.rg_find(self.acc_id,0, arg_rg_name=arg_amodule.params['rg_name']) + self.rg_id, self.rg_model = self.rg_find( + self.acc_id, + 0, + arg_rg_name=arg_amodule.params['rg_name'], + ) if arg_amodule.params['vins_id']: - self.vins_id, self.vins_facts = self.vins_find( + self.vins_id = self._vins_get_by_id( vins_id=arg_amodule.params['vins_id'] - ) - if not self.vins_id: - self.result['failed'] = True - self.result['msg'] = ( - f'Specified ViNS ID {arg_amodule.params["vins_id"]}' - f' not found' - ) - self.amodule.fail_json(**self.result) + ).id elif arg_amodule.params['vins_name']: - self.vins_id, self.vins_facts = self.vins_find( + self.vins_id, _ = self.vins_find( vins_id=arg_amodule.params['vins_id'], vins_name=arg_amodule.params['vins_name'], - rg_id=self.rg_id) + rg_id=self.rg_id, + ) if not self.vins_id: self.result['failed'] = True self.result['msg'] = ( @@ -94,10 +102,17 @@ class decort_lb(DecortController): self.amodule.fail_json(**self.result) if self.rg_id and arg_amodule.params['lb_name']: - self.lb_id, self.lb_facts = self.lb_find(0,arg_amodule.params['lb_name'],self.rg_id) + self.lb_id, self._lb_info = self.lb_find( + 0, + arg_amodule.params['lb_name'], + self.rg_id, + ) - if self.lb_id and self.lb_facts['status'] != 'DESTROYED': - self.acc_id = self.lb_facts['accountId'] + if ( + self._lb_info + and self._lb_info.status != sdk_types.LBStatus.DESTROYED + ): + self.acc_id = self._lb_info.account_id self.check_amodule_args_for_change() else: self.check_amodule_args_for_create() @@ -115,54 +130,73 @@ class decort_lb(DecortController): zone_id=self.aparams['zone_id'], start=start_after_create, ) - if self.lb_id and (self.amodule.params['backends'] or - self.amodule.params['frontends']): - self.lb_id, self.lb_facts = self.lb_find(0,self.amodule.params['lb_name'],self.rg_id) + if ( + self.lb_id and ( + self.amodule.params['backends'] + or self.amodule.params['frontends'] + ) + ): + self._lb_info = self._lb_get_by_id(lb_id=self.lb_id) self.lb_update( - lb_facts=self.lb_facts, + lb_model=self.lb_info, aparam_backends=self.amodule.params['backends'], aparam_frontends=self.amodule.params['frontends'], aparam_servers=self.amodule.params['servers'], ) return - - def action(self,d_state='',restore=False): - if restore == True: - self.lb_restore(lb_id=self.lb_id) - _, self.lb_facts = self._lb_get_by_id(lb_id=self.lb_id) - self.lb_state(self.lb_facts, 'enabled') - _, self.lb_facts = self._lb_get_by_id(lb_id=self.lb_id) - + + def action( + self, + d_state='', + restore=False, + ): + if restore: + self.sdk_checkmode(self.api.ca.lb.restore)( + lb_id=self.lb_id, + ) + self._lb_info = self._lb_get_by_id(lb_id=self.lb_id) + self.lb_state(self.lb_info, 'enabled') + self._lb_info = self._lb_get_by_id(lb_id=self.lb_id) + self.lb_update( - lb_facts=self.lb_facts, + lb_model=self.lb_info, aparam_backends=self.amodule.params['backends'], aparam_frontends=self.amodule.params['frontends'], aparam_servers=self.amodule.params['servers'], aparam_sysctl=self.aparams['sysctl'], ) + self._lb_info = self._lb_get_by_id(lb_id=self.lb_id) if d_state != '': - self.lb_state(self.lb_facts, d_state) - _, self.lb_facts = self._lb_get_by_id(lb_id=self.lb_id) + self.lb_state(self.lb_info, d_state) + self._lb_info = self._lb_get_by_id(lb_id=self.lb_id) - if (d_state == 'enabled' and - self.lb_facts.get('status') == 'ENABLED' and - self.lb_facts.get('techStatus') == 'STOPPED'): - self.lb_state(self.lb_facts, 'started') - _, self.lb_facts = self._lb_get_by_id(lb_id=self.lb_id) + if ( + d_state == 'enabled' + and self.lb_info.status == sdk_types.LBStatus.ENABLED + and self.lb_info.tech_status == sdk_types.LBTechStatus.STOPPED + ): + self.lb_state(self.lb_info, 'started') + self._lb_info = self._lb_get_by_id(lb_id=self.lb_id) aparam_zone_id = self.aparams['zone_id'] - if aparam_zone_id is not None and aparam_zone_id != self.lb_facts['zoneId']: + if ( + aparam_zone_id is not None + and aparam_zone_id != self.lb_info.zone_id + ): self.lb_migrate_to_zone( - lb_id=self.lb_id, + lb_id=self.lb_info.id, zone_id=aparam_zone_id, ) return def delete(self): - self.lb_delete(self.lb_id, self.amodule.params['permanently']) - self.lb_id, self.lb_facts = self._lb_get_by_id(self.lb_id) + self.sdk_checkmode(self.api.ca.lb.delete)( + lb_id=self.lb_id, + permanently=self.amodule.params['permanently'], + ) + self._lb_info = self._lb_get_by_id(lb_id=self.lb_id) return def nop(self): @@ -173,30 +207,39 @@ class decort_lb(DecortController): """ self.result['failed'] = False self.result['changed'] = False - if self.lb_id: - self.result['msg'] = ("No state change required for LB ID {} because of its " - "current status '{}'.").format(self.lb_id, self.lb_facts['status']) + if self._lb_info: + self.result['msg'] = ( + f'No state change required for LB ID {self._lb_info.id} ' + f'because of its current status "{self._lb_info.status}".' + ) else: - self.result['msg'] = ("No state change to '{}' can be done for " - "non-existent LB instance.").format(self.amodule.params['state']) + self.result['msg'] = ( + f'No state change to "{self.amodule.params['state']}" ' + f'can be done for non-existent LB instance.' + ) return + def error(self): self.result['failed'] = True self.result['changed'] = False - if self.vins_id: + if self._lb_info: self.result['failed'] = True self.result['changed'] = False - self.result['msg'] = ("Invalid target state '{}' requested for LB ID {} in the " - "current status '{}'").format(self.lb_id, - self.amodule.params['state'], - self.lb_facts['status']) + self.result['msg'] = ( + f'Invalid target state "{self.amodule.params['state']}" ' + f'requested for LB ID {self._lb_info.id} in the current status ' + f'"{self._lb_info.status}".' + ) else: self.result['failed'] = True self.result['changed'] = False - self.result['msg'] = ("Invalid target state '{}' requested for non-existent " - "LB name '{}'").format(self.amodule.params['state'], - self.amodule.params['lb_name']) + self.result['msg'] = ( + f'Invalid target state "{self.amodule.params['state']}" ' + f'requested for non-existent LB name ' + f'"{self.amodule.params['lb_name']}".' + ) return + def package_facts(self, arg_check_mode=False): """Package a dictionary of LB facts according to the decort_lb module specification. This dictionary will be returned to the upstream Ansible engine at the completion of @@ -205,34 +248,16 @@ class decort_lb(DecortController): @param arg_check_mode: boolean that tells if this Ansible module is run in check mode """ - ret_dict = dict(id=0, - name="none", - state="CHECK_MODE", - sysctl={}, - ) + ret_dict = {} if arg_check_mode: # in check mode return immediately with the default values return ret_dict - if self.lb_facts is None: - # if void facts provided - change state value to ABSENT and return - ret_dict['state'] = "ABSENT" + if self._lb_info is None: return ret_dict - ret_dict['id'] = self.lb_facts['id'] - ret_dict['name'] = self.lb_facts['name'] - ret_dict['state'] = self.lb_facts['status'] - ret_dict['account_id'] = self.lb_facts['accountId'] - ret_dict['rg_id'] = self.lb_facts['rgId'] - ret_dict['gid'] = self.lb_facts['gid'] - if self.amodule.params['state']!="absent": - ret_dict['backends'] = self.lb_facts['backends'] - ret_dict['frontends'] = self.lb_facts['frontends'] - ret_dict['sysctl'] = self.lb_facts['sysctlParams'] - ret_dict['zone_id'] = self.lb_facts['zoneId'] - ret_dict['tech_status'] = self.lb_facts['techStatus'] - return ret_dict + return self._lb_info.model_dump() @property def amodule_init_args(self) -> dict: @@ -321,18 +346,16 @@ class decort_lb(DecortController): def check_amodule_args_for_change(self): check_errors = False - - lb_info: dict = self.lb_facts - self.is_lb_stopped_or_will_be_stopped = ( + self.is_lb_stopped_or_will_be_stopped = ( ( - lb_info['techStatus'] == 'STOPPED' + self.lb_info.tech_status == sdk_types.LBTechStatus.STOPPED and ( self.aparams['state'] is None or self.aparams['state'] in ('present', 'stopped') ) ) or ( - lb_info['techStatus'] != 'STOPPED' + self.lb_info.tech_status != sdk_types.LBTechStatus.STOPPED and self.aparams['state'] == 'stopped' ) ) @@ -341,7 +364,7 @@ class decort_lb(DecortController): check_errors = True if ( self.aparams['zone_id'] is not None - and self.aparams['zone_id'] != lb_info['zoneId'] + and self.aparams['zone_id'] != self.lb_info.zone_id and not self.is_lb_stopped_or_will_be_stopped ): check_errors = True @@ -364,18 +387,32 @@ class decort_lb(DecortController): @DecortController.handle_sdk_exceptions def run(self): amodule = self.amodule - if self.lb_id: - if self.lb_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING","DESTROYING","RESTORING"]: + if self._lb_info: + if self._lb_info.status in [ + sdk_types.LBStatus.MODELED, + sdk_types.LBStatus.DISABLING, + sdk_types.LBStatus.ENABLING, + sdk_types.LBStatus.DELETING, + sdk_types.LBStatus.DESTROYING, + sdk_types.LBStatus.RESTORING, + ]: self.result['failed'] = True self.result['changed'] = False - self.result['msg'] = ("No change can be done for existing LB ID {} because of its current " - "status '{}'").format(self.lb_id, self.lb_facts['status']) - elif self.lb_facts['status'] in ('DISABLED', 'ENABLED', 'CREATED'): + self.result['msg'] = ( + f'No change can be done for existing LB ID ' + f'{self._lb_info.id} because of its current status ' + f'"{self._lb_info.status.name}".' + ) + elif self._lb_info.status in [ + sdk_types.LBStatus.DISABLED, + sdk_types.LBStatus.ENABLED, + sdk_types.LBStatus.CREATED, + ]: if amodule.params['state'] == 'absent': self.delete() else: self.action(d_state=amodule.params['state']) - elif self.lb_facts['status'] == "DELETED": + elif self._lb_info.status == sdk_types.LBStatus.DELETED: if amodule.params['state'] == 'present': self.action(restore=True) elif amodule.params['state'] == 'enabled': @@ -385,7 +422,7 @@ class decort_lb(DecortController): self.delete() elif amodule.params['state'] == 'disabled': self.error() - elif self.lb_facts['status'] == "DESTROYED": + elif self._lb_info.status == sdk_types.LBStatus.DESTROYED: if amodule.params['state'] in ('present', 'enabled'): self.create() elif amodule.params['state'] == 'absent': @@ -407,7 +444,7 @@ class decort_lb(DecortController): amodule.fail_json(**self.result) else: if self.result['changed']: - _, self.lb_facts = self.lb_find(lb_id=self.lb_id) + self._lb_info = self._lb_get_by_id(lb_id=self.lb_id) self.result['facts'] = self.package_facts(amodule.check_mode) amodule.exit_json(**self.result) diff --git a/library/decort_pfw.py b/library/decort_pfw.py index 15eef74..85ae378 100644 --- a/library/decort_pfw.py +++ b/library/decort_pfw.py @@ -43,7 +43,13 @@ class decort_pfw(DecortController): supports_check_mode=True, ) - def decort_pfw_package_facts(self, comp_facts, vins_facts, pfw_facts, check_mode=False): + def decort_pfw_package_facts( + self, + comp_facts, + vins_model: sdk_types.CloudapiVinsGetResultModel, + pfw_facts, + check_mode=False, + ): """Package a dictionary of PFW rules facts according to the decort_pfw module specification. This dictionary will be returned to the upstream Ansible engine at the completion of the module run. @@ -68,9 +74,13 @@ class decort_pfw(DecortController): ret_dict['state'] = "ABSENT" return ret_dict + gw_vnf = vins_model.vnfs.gw ret_dict['compute_id'] = comp_facts['id'] - ret_dict['vins_id'] = vins_facts['id'] - ret_dict['public_ip'] = vins_facts['vnfs']['GW']['config']['ext_net_ip'] + ret_dict['vins_id'] = vins_model.id + if gw_vnf: + ret_dict['public_ip'] = gw_vnf.config.ext_net_ip + else: + raise RuntimeError('VINS GW VNF must exist.') if len(pfw_facts) != 0: ret_dict['state'] = 'PRESENT' @@ -106,16 +116,15 @@ class decort_pfw(DecortController): self.result['msg'] = "Cannot find specified Compute ID {}.".format(amodule.params['compute_id']) amodule.fail_json(**self.result) - validated_vins_id, vins_facts = self.vins_find(amodule.params['vins_id']) - if not validated_vins_id: - self.result['failed'] = True - self.result['msg'] = "Cannot find specified ViNS ID {}.".format(amodule.params['vins_id']) - amodule.fail_json(**self.result) + vins_model = self._vins_get_by_id(vins_id=amodule.params['vins_id']) - gw_vnf_facts = vins_facts['vnfs'].get('GW') - if not gw_vnf_facts or gw_vnf_facts['status'] == "DESTROYED": + gw_vnf = vins_model.vnfs.gw + if not gw_vnf or gw_vnf.status == sdk_types.VNFStatus.DESTROYED: self.result['failed'] = True - self.result['msg'] = "ViNS ID {} does not have a configured external connection.".format(validated_vins_id) + self.result['msg'] = ( + f'ViNS ID {vins_model.id} does not ' + f'have a configured external connection.' + ) amodule.fail_json(**self.result) # @@ -124,12 +133,20 @@ class decort_pfw(DecortController): if amodule.params['state'] == 'absent': # ignore amodule.params['rules'] and remove all rules associated with this Compute - pfw_facts = self.pfw_configure(comp_facts, vins_facts, None) + pfw_facts = self.pfw_configure( + comp_facts, + vins_model, + None, + ) elif amodule.params['rules'] is not None: # manage PFW rules accodring to the module arguments - pfw_facts = self.pfw_configure(comp_facts, vins_facts, amodule.params['rules']) + pfw_facts = self.pfw_configure( + comp_facts, + vins_model, + amodule.params['rules'], + ) else: - pfw_facts = self._pfw_get(comp_facts['id'], vins_facts['id']) + pfw_facts = self._pfw_get(comp_facts['id'], vins_model.id) # # complete module run @@ -138,7 +155,12 @@ class decort_pfw(DecortController): amodule.fail_json(**self.result) else: # prepare PFW facts to be returned as part of self.result and then call exit_json(...) - self.result['facts'] = self.decort_pfw_package_facts(comp_facts, vins_facts, pfw_facts, amodule.check_mode) + self.result['facts'] = self.decort_pfw_package_facts( + comp_facts, + vins_model, + pfw_facts, + amodule.check_mode, + ) amodule.exit_json(**self.result) diff --git a/library/decort_rg.py b/library/decort_rg.py index 8bf204f..6dd914c 100644 --- a/library/decort_rg.py +++ b/library/decort_rg.py @@ -19,8 +19,7 @@ class decort_rg(DecortController): amodule = self.amodule self.validated_acc_id = 0 - self.validated_rg_id = 0 - self.validated_rg_facts = None + self.rg_id: int = 0 if amodule.params['rg_id'] is None: if self.amodule.params['account_id']: @@ -42,13 +41,15 @@ class decort_rg(DecortController): else: self.rg_should_exist = False - if self.validated_rg_id and self.rg_facts['status'] != 'DESTROYED': + if self.rg_id and ( + self.rg_info.status != sdk_types.ResourceGroupStatus.DESTROYED + ): self.check_amodule_args_for_change() def get_info(self): # If this is the first getting info - if not self.validated_rg_id: - self.validated_rg_id, self.rg_facts = self.rg_find( + if self._rg_info is None: + self.rg_id, self._rg_info = self.rg_find( arg_account_id=self.validated_acc_id, arg_rg_id=self.aparams['rg_id'], arg_rg_name=self.aparams['rg_name'], @@ -61,16 +62,16 @@ class decort_rg(DecortController): if self.amodule.check_mode: return - _, self.rg_facts = self.rg_find(arg_rg_id=self.validated_rg_id) + _, self._rg_info = self.rg_find(arg_rg_id=self.rg_id) def access(self): should_change_access = False acc_granted = False - for rg_item in self.rg_facts['acl']: - if rg_item['userGroupId'] == self.amodule.params['access']['user']: + for rg_item in self.rg_info.acl: + if rg_item.user_name == self.amodule.params['access']['user']: acc_granted = True if self.amodule.params['access']['action'] == 'grant': - if rg_item['right'] != self.amodule.params['access']['right']: + if rg_item.access_type.value != self.amodule.params['access']['right']: should_change_access = True if self.amodule.params['access']['action'] == 'revoke': should_change_access = True @@ -78,19 +79,30 @@ class decort_rg(DecortController): should_change_access = True if should_change_access == True: - self.rg_access(self.validated_rg_id, self.amodule.params['access']) - self.rg_facts['access'] = self.amodule.params['access'] + if self.amodule.params['access']['action'] == "grant": + self.sdk_checkmode(self.api.ca.rg.access_grant)( + access_type=sdk_types.AccessTypeForSet( + self.amodule.params['access']['right'] + ), + rg_id=self.rg_id, + user_name=self.amodule.params['access']['user'], + ) + else: + self.sdk_checkmode(self.api.ca.rg.access_revoke)( + rg_id=self.rg_id, + user_name=self.amodule.params['access']['user'], + ) self.rg_should_exist = True return def error(self): self.result['failed'] = True self.result['changed'] = False - if self.validated_rg_id > 0: + if self.rg_id: self.result['msg'] = ("Invalid target state '{}' requested for rg ID {} in the " - "current status '{}'.").format(self.validated_rg_id, + "current status '{}'.").format(self.rg_id, self.amodule.params['state'], - self.rg_facts['status']) + self.rg_info.status.value) else: self.result['msg'] = ("Invalid target state '{}' requested for non-existent rg name '{}' " "in account ID {} ").format(self.amodule.params['state'], @@ -99,20 +111,31 @@ class decort_rg(DecortController): return def update(self): - resources = self.rg_facts['Resources']['Reserved'] + try: + rg_res_model = ( + self.api.cloudapi.rg.get_resource_consumption(rg_id=self.rg_id) + ) + except sdk_exceptions.RequestException as e: + self.message( + msg=( + f'Failed to get RG Resources by ID {self.rg_id}: {e}' + ) + ) + self.exit(fail=True) + reserved_resources = rg_res_model.reserved incorrect_quota = dict(Requested=dict(), Reserved=dict(),) query_key_map = dict( - cpu='cpu', - ram='ram', - disk='disksize', - ext_ips='extips', - storage_policies='policies', + cpu='cpu_count', + ram='ram_size_mb', + disk='storage_size_gb_by_real_usage', + ext_ips='ext_ip_count', + storage_policies='storage_policies', ) if self.amodule.params['quotas']: for quota_item in self.amodule.params['quotas']: if quota_item == 'storage_policies': - rg_storage_policies = resources[query_key_map[quota_item]] + rg_storage_policies = reserved_resources.storage_policies aparam_storage_policies = self.amodule.params['quotas'][ quota_item ] @@ -124,32 +147,35 @@ class decort_rg(DecortController): if ( rg_storage_policy and aparam_storage_policy['storage_size_gb'] - < rg_storage_policy['disksize'] + < rg_storage_policy.storage_size_gb_by_real_usage ): incorrect_quota['Requested'][quota_item] = ( self.amodule.params['quotas'][quota_item] ) incorrect_quota['Reserved'][quota_item] = ( - resources[query_key_map[quota_item]] + getattr( + reserved_resources, + query_key_map[quota_item] + ) ) elif ( self.amodule.params['quotas'][quota_item] - < resources[query_key_map[quota_item]] + < getattr(reserved_resources, query_key_map[quota_item]) ): incorrect_quota['Requested'][quota_item] = ( self.amodule.params['quotas'][quota_item] ) incorrect_quota['Reserved'][quota_item] = ( - resources[query_key_map[quota_item]] + getattr(reserved_resources, query_key_map[quota_item]) ) - if incorrect_quota['Requested']: + if reserved_resources and incorrect_quota['Requested']: self.result['msg'] = ("Cannot limit less than already reserved'{}'").format(incorrect_quota) self.result['failed'] = True if not self.result['failed']: self.rg_update( - arg_rg_dict=self.rg_facts, + rg_model=self.rg_info, arg_quotas=self.amodule.params['quotas'], arg_res_types=self.amodule.params['resType'], arg_newname=self.amodule.params['rename'], @@ -160,13 +186,15 @@ class decort_rg(DecortController): return def setDefNet(self): - rg_def_net_type = self.rg_facts['def_net_type'] - rg_def_net_id = self.rg_facts['def_net_id'] + rg_def_net_type = self.rg_info.default_net_type.value + rg_def_net_id = self.rg_info.default_net_id aparam_def_net_type = self.aparams['def_netType'] aparam_def_net_id = self.aparams['def_netId'] - need_to_reset = (aparam_def_net_type == 'NONE' - and rg_def_net_type != aparam_def_net_type) + need_to_reset = ( + aparam_def_net_type == sdk_types.RGDefaultNetType.NONE + and rg_def_net_type != aparam_def_net_type + ) need_to_change = False if aparam_def_net_id is not None: @@ -174,16 +202,26 @@ class decort_rg(DecortController): or aparam_def_net_type != rg_def_net_type) if need_to_reset or need_to_change: - self.rg_setDefNet( - arg_rg_id=self.validated_rg_id, - arg_net_type=aparam_def_net_type, - arg_net_id=aparam_def_net_id, - ) + if aparam_def_net_type == sdk_types.RGDefaultNetType.NONE: + self.sdk_checkmode(self.api.ca.rg.remove_def_net)( + rg_id=self.rg_id, + ) + else: + if aparam_def_net_type == sdk_types.RGDefaultNetType.PRIVATE: + net_type = sdk_types.RGDefaultNetTypeForSet.PRIVATE + elif aparam_def_net_type == sdk_types.RGDefaultNetType.PUBLIC: + net_type = sdk_types.RGDefaultNetTypeForSet.PUBLIC + + self.sdk_checkmode(self.api.ca.rg.set_def_net)( + net_type=net_type, + rg_id=self.rg_id, + net_id=aparam_def_net_id, + ) self.rg_should_exist = True return def create(self): - self.validated_rg_id = self.rg_provision( + self.rg_id = self.rg_provision( self.validated_acc_id, self.amodule.params['rg_name'], self.amodule.params['owner'], @@ -198,10 +236,10 @@ class decort_rg(DecortController): sdn_access_group_id=self.aparams['sdn_access_group_id'], ) - if self.validated_rg_id: - self.validated_rg_id, self.rg_facts = self.rg_find( + if self.rg_id: + self.rg_id, self._rg_info = self.rg_find( arg_account_id=self.validated_acc_id, - arg_rg_id=self.validated_rg_id, + arg_rg_id=self.rg_id, arg_rg_name="", arg_check_state=False ) @@ -209,31 +247,30 @@ class decort_rg(DecortController): return def enable(self): - self.rg_enable(self.validated_rg_id, - self.amodule.params['state']) if self.amodule.params['state'] == "enabled": - self.rg_facts['status'] = 'CREATED' - else: - self.rg_facts['status'] = 'DISABLED' + self.sdk_checkmode(self.api.ca.rg.enable)( + rg_id=self.rg_id, + ) + elif self.amodule.params['state'] == "disabled": + self.sdk_checkmode(self.api.ca.rg.disable)( + rg_id=self.rg_id, + ) self.rg_should_exist = True return def restore(self): - self.rg_restore(self.validated_rg_id) - self.rg_facts['status'] = 'DISABLED' + self.sdk_checkmode(self.api.ca.rg.restore)( + rg_id=self.rg_id, + ) self.rg_should_exist = True return def destroy(self): - self.rg_delete( - rg_id=self.validated_rg_id, + self.sdk_checkmode(self.api.ca.rg.delete)( + rg_id=self.rg_id, permanently=self.amodule.params['permanently'], recursively=self.aparams['recursive_deletion'], ) - if self.amodule.params['permanently'] == True: - self.rg_facts['status'] = 'DESTROYED' - else: - self.rg_facts['status'] = 'DELETED' self.rg_should_exist = False return @@ -259,23 +296,7 @@ class decort_rg(DecortController): # ret_dict['state'] = "ABSENT" # return ret_dict - ret_dict['id'] = self.rg_facts['id'] - ret_dict['name'] = self.rg_facts['name'] - ret_dict['state'] = self.rg_facts['status'] - ret_dict['account_id'] = self.rg_facts['accountId'] - ret_dict['gid'] = self.rg_facts['gid'] - ret_dict['quota'] = self.rg_facts['resourceLimits'] - ret_dict['resTypes'] = self.rg_facts['resourceTypes'] - ret_dict['defNetId'] = self.rg_facts['def_net_id'] - ret_dict['defNetType'] = self.rg_facts['def_net_type'] - ret_dict['ViNS'] = self.rg_facts['vins'] - ret_dict['computes'] = self.rg_facts['vms'] - ret_dict['uniqPools'] = self.rg_facts['uniqPools'] - ret_dict['description'] = self.rg_facts['desc'] - ret_dict['sdn_access_group_id'] = self.rg_facts['sdn_access_group_id'] - ret_dict['storage_policy_ids'] = self.rg_facts['storage_policy_ids'] - - return ret_dict + return self.rg_info.model_dump() @property def amodule_init_args(self) -> dict: @@ -290,6 +311,24 @@ class decort_rg(DecortController): ), access=dict( type='dict', + options=dict( + action=dict( + type='str', + required=True, + choices=[ + 'grant', + 'revoke', + ], + ), + user=dict( + type='str', + required=True, + ), + right=dict( + type='str', + choices=sdk_types.AccessTypeForSet._member_names_, + ), + ), ), description=dict( type='str', @@ -297,12 +336,8 @@ class decort_rg(DecortController): ), def_netType=dict( type='str', - choices=[ - 'PRIVATE', - 'PUBLIC', - 'NONE', - ], - default='PRIVATE', + choices=sdk_types.RGDefaultNetType._member_names_, + default=sdk_types.RGDefaultNetType.PRIVATE.name, ), def_netId=dict( type='int', @@ -386,13 +421,13 @@ class decort_rg(DecortController): self.aparams['sdn_access_group_id'] is not None and ( self.aparams['sdn_access_group_id'] - != self.rg_facts['sdn_access_group_id'] + != self.rg_info.sdn_access_group_id ) ): self.message( 'Check for parameter "sdn_access_group_id" failed: ' 'cannot change sdn_access_group_id for an existing resource ' - f'group ID {self.validated_rg_id}.' + f'group ID {self.rg_id}.' ) check_errors = True @@ -410,10 +445,15 @@ class decort_rg(DecortController): def run(self): amodule = self.amodule #amodule.check_mode=True - if self.validated_rg_id > 0: - if self.rg_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING", "CONFIRMED"]: + if self.rg_id: + if self.rg_info.status in [ + sdk_types.ResourceGroupStatus.MODELED, + sdk_types.ResourceGroupStatus.DISABLING, + sdk_types.ResourceGroupStatus.ENABLING, + sdk_types.ResourceGroupStatus.DESTROYING, + ]: self.error() - elif self.rg_facts['status'] in ("CREATED"): + elif self.rg_info.status == sdk_types.ResourceGroupStatus.CREATED: if amodule.params['state'] == 'absent': self.destroy() elif amodule.params['state'] == "disabled": @@ -432,7 +472,7 @@ class decort_rg(DecortController): if amodule.params['def_netType'] is not None: self.setDefNet() - elif self.rg_facts['status'] == "DELETED": + elif self.rg_info.status == sdk_types.ResourceGroupStatus.DELETED: if amodule.params['state'] == 'absent' and amodule.params['permanently'] == True: self.destroy() elif (amodule.params['state'] == 'present' @@ -441,7 +481,7 @@ class decort_rg(DecortController): elif amodule.params['state'] == 'enabled': self.restore() self.enable() - elif self.rg_facts['status'] in ("DISABLED"): + elif self.rg_info.status == sdk_types.ResourceGroupStatus.DISABLED: if amodule.params['state'] == 'absent': self.destroy() elif amodule.params['state'] == ("enabled"): diff --git a/library/decort_sdn_access_group.py b/library/decort_sdn_access_group.py new file mode 100644 index 0000000..472526e --- /dev/null +++ b/library/decort_sdn_access_group.py @@ -0,0 +1,213 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_access_group + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests + + +class DecortSDNAccessGroups(DecortController): + access_group_id: str | None = None + _access_group_info: dict[str, Any] | None = None + need_final_get: bool = True + + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + access_group_id=dict( + type='str', + ), + comment=dict( + type='str', + ), + display_name=dict( + type='str', + ), + state=dict( + type='str', + choices=['present', 'absent'], + ), + ), + supports_check_mode=True, + ) + + @property + def access_group_info(self) -> dict[str, Any]: + if self._access_group_info is None: + if not isinstance(self.access_group_id, str): + raise TypeError + access_group_info = self.get() + if access_group_info is None: + raise TypeError + self._access_group_info = access_group_info + return self._access_group_info + + def check_amodule_args(self): + check_errors = False + + if ( + self.aparams['state'] == 'absent' + and self.aparams['access_group_id'] is None + ): + check_errors = True + self.message( + 'Check for parameter "access_group_id" failed: ' + 'access_group_id must be specified when state is "absent".' + ) + + if check_errors: + self.exit(fail=True) + + def check_amodule_args_for_create(self): + check_errors = False + + if self.aparams['comment'] is None: + check_errors = True + self.message( + 'Check for parameter "comment" failed: parameter must ' + 'be specified for creating an access_group.' + ) + if self.aparams['display_name'] is None: + check_errors = True + self.message( + 'Check for parameter "display_name" failed: parameter must ' + 'be specified for creating an access_group.' + ) + + if check_errors: + self.exit(fail=True) + + @DecortController.waypoint + @DecortController.checkmode + def get(self) -> dict[str, Any] | None: + params = {'access_group_id': self.access_group_id} + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/access_group/get', + arg_params=params, + not_fail_codes=[404], + accept_json_response=True, + ) + if response.status_code == 404: + self.message( + self.MESSAGES.obj_not_found( + obj='access_group', + id=self.access_group_id, + ) + ) + self.exit(fail=True) + return response.json() + + @DecortController.waypoint + @DecortController.checkmode + def access_group_find(self, display_name: str) -> dict[str, Any] | None: + params = {'display_name': display_name} + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/access_group/list', + arg_params=params, + accept_json_response=True, + ) + for access_group in response.json() or []: + if access_group['display_name'] == display_name: + return access_group + return None + + @DecortController.waypoint + @DecortController.checkmode + def create(self): + params = { + 'comment': self.aparams['comment'], + 'display_name': self.aparams['display_name'], + } + response = self.decort_api_call( + arg_req_function=requests.post, + arg_api_name='/restmachine/sdn/access_group/create', + arg_params=params, + accept_json_response=True, + ) + self.access_group_id = response.json()['id'] + self.set_changed() + + @DecortController.waypoint + @DecortController.checkmode + def delete(self): + params = {'access_group_id': self.access_group_id} + response = self.decort_api_call( + arg_req_function=requests.delete, + arg_api_name='/restmachine/sdn/access_group/delete', + arg_params=params, + not_fail_codes=[204, 404] + ) + self.need_final_get = False + if response.status_code == 204: + self.set_changed() + self.message( + self.MESSAGES.obj_deleted( + obj='access_group', + id=self.access_group_id, + ) + ) + else: + self.message( + self.MESSAGES.obj_not_found( + obj='access_group', + id=self.access_group_id, + ) + ) + self.facts = {} + + def run(self): + self.check_amodule_args() + + if self.aparams['access_group_id']: + self.access_group_id = self.aparams['access_group_id'] + elif self.aparams['state'] != 'absent': + self.check_amodule_args_for_create() + access_group_info = self.access_group_find( + display_name=self.aparams['display_name'], + ) + if access_group_info: + self.access_group_id = access_group_info['id'] + self._access_group_info = access_group_info + + if self.access_group_id: + if self.aparams['state'] == 'absent': + self.delete() + else: + state = self.aparams['state'] + if state is None: + state = 'present' + self.message( + msg=self.MESSAGES.default_value_used( + param_name='state', + default_value=state, + ), + warning=True, + ) + + if state != 'absent': + self.create() + + if self.need_final_get: + self.facts = self.get() + self.exit() + + +def main(): + DecortSDNAccessGroups().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_sdn_access_group_list.py b/library/decort_sdn_access_group_list.py new file mode 100644 index 0000000..8840779 --- /dev/null +++ b/library/decort_sdn_access_group_list.py @@ -0,0 +1,120 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_access_group_list + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests + + +class DecortSDNAccessGroupList(DecortController): + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + filter=dict( + type='dict', + apply_defaults=True, + options=dict( + enabled=dict( + type='bool', + ), + deleted=dict( + type='bool', + ), + display_name=dict( + type='str', + ), + owner_display_name=dict( + type='str', + ), + created_from=dict( + type='str', + ), + created_to=dict( + type='str', + ), + ), + ), + pagination=dict( + type='dict', + apply_defaults=True, + options=dict( + number=dict( + type='int', + default=1, + ), + size=dict( + type='int', + ), + ), + ), + sorting=dict( + type='dict', + options=dict( + asc=dict( + type='bool', + default=True, + ), + field=dict( + type='str', + choices=[ + 'display_name', + 'created_at', + 'updated_at', + 'deleted_at', + 'owner_login', + ], + required=True, + ), + ), + ), + ), + supports_check_mode=True, + ) + + def run(self): + self.get_info() + self.exit() + + def get_info(self): + params: dict[str, Any] = dict() + + aparam_filter: dict[str, Any] = self.aparams['filter'] + for field, value in aparam_filter.items(): + if value is not None: + params[field] = value + + aparam_pagination: dict[str, Any] = self.aparams['pagination'] + params['page'] = aparam_pagination['number'] + params['per_page'] = aparam_pagination['size'] + + aparam_sorting: dict[str, Any] | None = self.aparams['sorting'] + if aparam_sorting: + params['sort_by'] = aparam_sorting['field'] + params['sort_order'] = 'asc' if aparam_sorting['asc'] else 'desc' + + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/access_group/list', + arg_params=params, + accept_json_response=True, + ) + self.facts = response.json() + + +def main(): + DecortSDNAccessGroupList().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_sdn_hypervisor.py b/library/decort_sdn_hypervisor.py new file mode 100644 index 0000000..0b667c3 --- /dev/null +++ b/library/decort_sdn_hypervisor.py @@ -0,0 +1,160 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_hypervisor + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests + + +class DecortSDNHypervisor(DecortController): + name: str | None = None + _hypervisor_info: dict[str, Any] | None = None + need_final_get: bool = True + + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + display_name=dict( + type='str', + ), + name=dict( + type='str', + ), + port_info=dict( + type='str', + choices=['detailed', 'general'], + ), + state=dict( + type='str', + choices=['absent'], + ), + ), + supports_check_mode=True, + ) + + @property + def hypervisor_info(self) -> dict[str, Any]: + if self._hypervisor_info is None: + if not isinstance(self.name, str): + raise TypeError + hypervisor_info = self.get() + if hypervisor_info is None: + raise TypeError + self._hypervisor_info = hypervisor_info + return self._hypervisor_info + + def check_amodule_args(self): + check_errors = False + + if self.aparams['name'] is None: + check_errors = True + self.message( + 'Check for parameter "name" failed: ' + 'name must be specified.' + ) + + if check_errors: + self.exit(fail=True) + + @DecortController.waypoint + def get(self) -> dict[str, Any] | None: + params = {'name': self.name} + if self.aparams['port_info'] is not None: + params['port_info'] = self.aparams['port_info'] + + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/hypervisor/get', + arg_params=params, + not_fail_codes=[400], + accept_json_response=True, + ) + if response.status_code == 400: + self.message( + self.MESSAGES.obj_not_found( + obj='hypervisor', + id=self.name, + ) + ) + self.exit(fail=True) + return response.json() + + @DecortController.waypoint + @DecortController.checkmode + def update_display_name(self): + params = { + 'name': self.aparams['name'], + 'display_name': self.aparams['display_name'], + } + self.decort_api_call( + arg_req_function=requests.put, + arg_api_name='/restmachine/sdn/hypervisor/update_display_name', + arg_params=params, + accept_json_response=True, + ) + self.set_changed() + + @DecortController.waypoint + @DecortController.checkmode + def delete(self): + params = {'name': self.name} + response = self.decort_api_call( + arg_req_function=requests.delete, + arg_api_name='/restmachine/sdn/hypervisor/delete', + arg_params=params, + not_fail_codes=[400] + ) + self.need_final_get = False + if response.status_code == 200: + self.set_changed() + self.message( + self.MESSAGES.obj_deleted( + obj='hypervisor', + id=self.name, + ) + ) + else: + self.message( + self.MESSAGES.obj_not_found( + obj='hypervisor', + id=self.name, + ) + ) + self.facts = {} + + def run(self): + self.check_amodule_args() + self.name = self.aparams['name'] + + if self.aparams['state'] == 'absent': + self.delete() + else: + if ( + self.aparams['display_name'] is not None + and self.aparams['display_name'] + != self.hypervisor_info.get('display_name') + ): + self.update_display_name() + + if self.need_final_get: + self.facts = self.get() + self.exit() + + +def main(): + DecortSDNHypervisor().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_sdn_hypervisor_list.py b/library/decort_sdn_hypervisor_list.py new file mode 100644 index 0000000..d83004d --- /dev/null +++ b/library/decort_sdn_hypervisor_list.py @@ -0,0 +1,135 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_hypervisor_list + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests + + +class DecortSDNHypervisorList(DecortController): + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + port_info=dict( + type='str', + choices=['detailed', 'general'] + ), + filter=dict( + type='dict', + apply_defaults=True, + options=dict( + hostname=dict( + type='str', + ), + display_name=dict( + type='str', + ), + ip=dict( + type='str', + ), + created_from=dict( + type='str', + ), + created_to=dict( + type='str', + ), + updated_from=dict( + type='str', + ), + updated_to=dict( + type='str', + ), + ), + ), + pagination=dict( + type='dict', + apply_defaults=True, + options=dict( + number=dict( + type='int', + default=1, + ), + size=dict( + type='int', + ), + ), + ), + sorting=dict( + type='dict', + options=dict( + asc=dict( + type='bool', + default=True, + ), + field=dict( + type='str', + choices=[ + 'name', + 'hostname', + 'last_sync', + 'display_name', + 'ip', + 'created_at', + 'updated_at', + ], + required=True, + ), + ), + ), + ), + supports_check_mode=True, + ) + + def run(self): + self.get_info() + self.exit() + + def get_info(self): + api_params: dict[str, Any] = dict() + + aparam_port_info = self.aparams['port_info'] + if aparam_port_info is not None: + api_params['port_info'] = aparam_port_info + + aparam_filter: dict[str, Any] = self.aparams['filter'] + for field, value in aparam_filter.items(): + if value is not None: + api_params[field] = value + + aparam_pagination: dict[str, Any] = self.aparams['pagination'] + api_params['page'] = aparam_pagination['number'] + api_params['per_page'] = aparam_pagination['size'] + + aparam_sorting: dict[str, Any] | None = self.aparams['sorting'] + if aparam_sorting: + api_params['sort_by'] = aparam_sorting['field'] + api_params['sort_order'] = ( + 'asc' if aparam_sorting['asc'] else 'desc' + ) + + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/hypervisor/list', + arg_params=api_params, + accept_json_response=True, + ) + self.facts = response.json() + + +def main(): + DecortSDNHypervisorList().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_sdn_logical_port.py b/library/decort_sdn_logical_port.py new file mode 100644 index 0000000..434c9f1 --- /dev/null +++ b/library/decort_sdn_logical_port.py @@ -0,0 +1,588 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_logical_port + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests +import time + + +class DecortSDNLogicalPort(DecortController): + FIELDS_FOR_CREATE = ( + 'access_group_id', + 'adapter_mac', + 'description', + 'display_name', + 'hypervisor', + 'labels', + 'port_security', + 'segment_id', + 'unique_identifier', + ) + + FIELDS_FOR_UPDATE = ( + 'adapter_mac', + 'description', + 'display_name', + 'hypervisor', + 'labels', + 'logical_port_id', + 'port_security', + 'segment_id', + 'version_id', + ) + + UPDATE_FIELDS = ( + 'adapter_mac', + 'description', + 'display_name', + 'hypervisor', + 'labels', + 'port_security', + 'segment_id', + ) + + logical_port_id: str | None = None + _logical_port_info: dict[str, Any] | None = None + need_final_get: bool = True + + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + no_migration=dict( + type='bool', + ), + hypervisor_change_via_migration=dict( + type='bool', + default=False, + ), + logical_port_id=dict( + type='str', + ), + state=dict( + type='str', + choices=[ + 'present', + 'enabled', + 'disabled', + 'absent', + 'absent_force', + ], + ), + access_group_id=dict( + type='str', + ), + adapter_mac=dict( + type='str', + ), + description=dict( + type='str', + ), + display_name=dict( + type='str', + ), + hypervisor=dict( + type='str', + ), + labels=dict( + type='dict', + ), + no_addresses=dict( + type='bool', + ), + port_security=dict( + type='bool', + ), + segment_id=dict( + type='str', + ), + unique_identifier=dict( + type='str', + ), + version_id=dict( + type='int', + ), + ), + supports_check_mode=True, + mutually_exclusive=[ + ('unique_identifier', 'logical_port_id'), + ('no_migration', 'hypervisor_change_via_migration'), + ], + ) + + def run(self): + self.check_amodule_args() + state = self.aparams['state'] + if self.aparams['unique_identifier'] is not None: + self._logical_port_info = ( + self.logical_port_get_by_unique_identifier( + fail_if_not_found=False, + ) + ) + self.logical_port_id = self._logical_port_info['id'] + elif self.aparams['logical_port_id']: + self.logical_port_id = self.aparams['logical_port_id'] + self._logical_port_info = self.logical_port_get( + fail_if_not_found=False, + ) + elif self.aparams['display_name']: + self._logical_port_info = self.logical_port_find( + display_name=self.aparams['display_name'] + ) + if self._logical_port_info: + self.logical_port_id = self._logical_port_info['id'] + + if self._logical_port_info is not None: + if state in ('absent', 'absent_force'): + self.logical_port_delete( + force=state == 'absent_force', + ) + elif self.aparams['no_migration']: + self.logical_port_migration_cancel() + elif self.aparams['hypervisor_change_via_migration']: + self.logical_port_migration_start() + else: + self.logical_port_update( + desired_enabled=( + True if state == 'enabled' + else False if state == 'disabled' + else None + ) + ) + else: + if state == 'present': + self.check_amodule_args_for_create() + self.logical_port_create() + elif state in ('enabled', 'disabled'): + self.message( + 'Check for parameter "state" failed: state values ' + '"enabled"/"disabled" can only be applied to an existing ' + 'logical port.' + ) + self.exit(fail=True) + else: + self.need_final_get = False + self.facts = {} + + if self.need_final_get and not self.amodule.check_mode: + if self._logical_port_info is not None: + self.facts = self.logical_port_info + else: + self.facts = self.logical_port_get() + self.exit() + + def check_amodule_args(self): + check_errors = False + + if ( + self.aparams['no_migration'] + and self.aparams['state'] == 'absent' + ): + check_errors = True + self.message( + 'Check for parameter "no_migration" failed: ' + 'no_migration cannot be used when state is "absent".' + ) + + if ( + self.aparams['hypervisor_change_via_migration'] + and self.aparams['hypervisor'] is None + ): + check_errors = True + self.message( + 'Check for parameters ' + '"hypervisor_change_via_migration/hypervisor" failed: ' + '"hypervisor" must be specified when ' + '"hypervisor_change_via_migration" is true.' + ) + + if ( + self.aparams['no_migration'] + and self.aparams['hypervisor'] is not None + ): + check_errors = True + self.message( + 'Check for parameters "no_migration/hypervisor" failed: ' + '"hypervisor" cannot be used when "no_migration" is true.' + ) + + if check_errors: + self.exit(fail=True) + + def check_amodule_args_for_create(self): + check_errors = False + + for field in ( + 'access_group_id', + 'description', + 'display_name', + 'hypervisor', + 'port_security', + 'segment_id', + ): + if self.aparams[field] is None: + check_errors = True + self.message( + f'Check for parameter "{field}" failed: parameter ' + f'"{field}" is required when creating a logical port.' + ) + + if check_errors: + self.exit(fail=True) + + @property + def logical_port_info(self) -> dict[str, Any]: + if self._logical_port_info is None: + if ( + self.aparams['unique_identifier'] is None + and self.aparams['logical_port_id'] is None + ): + raise TypeError + logical_port_info = self.logical_port_get() + if logical_port_info is None: + raise TypeError + self._logical_port_info = logical_port_info + return self._logical_port_info + + @DecortController.waypoint + def logical_port_get_by_unique_identifier( + self, + fail_if_not_found: bool = True, + ) -> dict[str, Any] | None: + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name=( + '/restmachine/sdn/logical_port/get_by_unique_identifier' + ), + arg_params={ + 'unique_identifier': self.aparams['unique_identifier'] + }, + not_fail_codes=[404], + accept_json_response=True, + ) + if response.status_code == 404: + if fail_if_not_found: + self.message( + self.MESSAGES.obj_not_found( + obj='logical port', + id=self.aparams['unique_identifier'], + ) + ) + self.exit(fail=True) + return None + return response.json() + + @DecortController.waypoint + def logical_port_get( + self, + fail_if_not_found: bool = True, + ) -> dict[str, Any] | None: + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/logical_port/get', + arg_params={'logical_port_id': self.logical_port_id}, + accept_json_response=True, + not_fail_codes=[404], + ) + if response.status_code == 404: + if fail_if_not_found: + self.message( + self.MESSAGES.obj_not_found( + obj='logical port', + id=self.logical_port_id, + ) + ) + self.exit(fail=True) + return None + return response.json() + + @DecortController.waypoint + def logical_port_find(self, display_name: str) -> dict[str, Any] | None: + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/logical_port/list', + arg_params={'display_name': display_name}, + accept_json_response=True, + ) + return response.json()[0] if response.json() else None + + @DecortController.waypoint + @DecortController.checkmode + def logical_port_create(self): + payload = {} + for field in self.FIELDS_FOR_CREATE: + value = self.aparams[field] + if value is not None: + payload[field] = value + payload['enabled'] = True + + response = self.decort_api_call( + arg_req_function=requests.post, + arg_api_name='/restmachine/sdn/logical_port/create', + arg_json_body=payload, + accept_json_response=True, + ) + self.logical_port_id = response.json()['id'] + self.set_changed() + + @DecortController.waypoint + @DecortController.checkmode + def logical_port_delete( + self, + force: bool = False, + ): + version_id = self.aparams['version_id'] + if version_id is None and self.logical_port_info is not None: + version_id = self.logical_port_info['version_id'] + + payload = { + 'logical_port_id': self.logical_port_id, + 'version_id': version_id, + 'force': force, + } + + self.decort_api_call( + arg_req_function=requests.delete, + arg_api_name='/restmachine/sdn/logical_port/delete', + arg_json_body=payload, + ) + self.need_final_get = False + self.facts = {} + self.set_changed() + self.message( + msg=self.MESSAGES.obj_deleted( + obj='logical port', + id=self.logical_port_id, + permanently=True, + ) + ) + + @DecortController.waypoint + @DecortController.checkmode + def logical_port_update( + self, + desired_enabled: bool | None = None, + ): + need_update = False + + addresses_to_remove = [] + for field in self.UPDATE_FIELDS: + value = self.aparams[field] + if value is None: + continue + if self.aparams['no_addresses']: + bindings = self.logical_port_info['bindings'] + if bindings and bindings.get('logical_port_addresses'): + for address in bindings['logical_port_addresses']: + addresses_to_remove.append(address['id']) + if addresses_to_remove: + need_update = True + + if field == 'port_security': + current_value = self.logical_port_info['bindings'].get( + 'port_security' + ) + elif field == 'segment_id': + current_value = self.logical_port_info['bindings'].get( + 'segment_id' + ) + else: + current_value = self.logical_port_info.get(field) + if isinstance(value, dict) and isinstance(current_value, dict): + if any( + current_value.get(key) != expected_value + for key, expected_value in value.items() + ): + need_update = True + break + continue + if value != current_value: + need_update = True + break + + if ( + not need_update + and desired_enabled is not None + and desired_enabled != self.logical_port_info['enabled'] + ): + need_update = True + + if need_update: + payload = { + 'logical_port_id': self.logical_port_id, + 'version_id': ( + self.aparams['version_id'] + or self.logical_port_info['version_id'] + ), + 'adapter_mac': ( + self.aparams['adapter_mac'] + or self.logical_port_info['adapter_mac'] + ), + 'description': ( + self.aparams['description'] + or self.logical_port_info['description'] + ), + 'display_name': ( + self.aparams['display_name'] + or self.logical_port_info['display_name'] + ), + 'enabled': ( + desired_enabled + if desired_enabled is not None + else self.logical_port_info['enabled'] + ), + 'hypervisor': ( + self.aparams['hypervisor'] + or self.logical_port_info['hypervisor'] + ), + 'labels': ( + self.aparams['labels'] + or self.logical_port_info.get('labels') + ), + 'port_security': ( + self.aparams['port_security'] + if self.aparams['port_security'] is not None + else self.logical_port_info['bindings']['port_security'] + ), + 'segment_id': ( + self.aparams['segment_id'] + or self.logical_port_info['bindings']['segment_id'] + ), + } + if addresses_to_remove: + payload['remove_addresses'] = addresses_to_remove + + self.decort_api_call( + arg_req_function=requests.put, + arg_api_name='/restmachine/sdn/logical_port/update', + arg_json_body=payload, + accept_json_response=True, + ) + self._logical_port_info = None + self.set_changed() + + @DecortController.waypoint + @DecortController.checkmode + def logical_port_migration_cancel(self): + version_id = self.aparams['version_id'] + if version_id is None and self._logical_port_info is not None: + version_id = self._logical_port_info['version_id'] + + self.decort_api_call( + arg_req_function=requests.delete, + arg_api_name='/restmachine/sdn/logical_port/migration_cancel', + arg_json_body={ + 'logical_port_id': self.logical_port_id, + 'version_id': version_id, + }, + ) + self.set_changed() + + @DecortController.waypoint + @DecortController.checkmode + def logical_port_migration_start(self): + self.decort_api_call( + arg_req_function=requests.post, + arg_api_name='/restmachine/sdn/logical_port/migration_start', + arg_json_body={ + 'logical_port_id': self.logical_port_id, + 'target_hypervisor': self.aparams['hypervisor'], + 'version_id': ( + self.aparams['version_id'] + or self.logical_port_info['version_id'] + if self._logical_port_info is not None + else self.aparams['version_id'] + ), + }, + accept_json_response=True, + ) + waiting_statuses = { + 'Idle', + 'SynchronizingAtCore', + 'SynchronizingAtOVN', + } + failed_statuses = { + 'NoHypervisorAtOVN': ( + 'Logical port migration failed: no hypervisor at OVN.' + ), + 'FailedAtCore': 'Logical port migration failed at core.', + 'TemporaryFailedAtCore': ( + 'Logical port migration temporary failed at core.' + ), + } + target_hypervisor = self.aparams['hypervisor'] + + for _ in range(300): + logical_port_info = self.logical_port_get() + if logical_port_info is None: + self.message( + 'Logical port migration failed: ' + 'can\'t get logical port info.' + ) + self.exit(fail=True) + status_info = logical_port_info.get('status') + if not isinstance(status_info, dict): + status_info = {} + hypervisors = status_info.get('hypervisors', []) + + target_hypervisor_status = None + for hypervisor in hypervisors: + if hypervisor.get('name') == target_hypervisor: + target_hypervisor_status = str( + hypervisor.get('operation_status') + ) + break + + if target_hypervisor_status == 'Synchronized': + self._logical_port_info = logical_port_info + self.set_changed() + return + + if target_hypervisor_status == 'NoHypervisorAtOVN': + self.message(failed_statuses['NoHypervisorAtOVN']) + self.exit(fail=True) + if target_hypervisor_status == 'FailedAtCore': + self.message(failed_statuses['FailedAtCore']) + self.exit(fail=True) + if target_hypervisor_status == 'TemporaryFailedAtCore': + self.message(failed_statuses['TemporaryFailedAtCore']) + self.exit(fail=True) + + if target_hypervisor_status in waiting_statuses: + time.sleep(5) + continue + + if target_hypervisor_status is None: + self.message( + 'Logical port migration failed: target hypervisor ' + f'"{target_hypervisor}" not found in status.' + ) + else: + self.message( + 'Logical port migration failed with unexpected status ' + f'for hypervisor "{target_hypervisor}": ' + f'{target_hypervisor_status}.' + ) + self.exit(fail=True) + + self.message('Logical port migration timed out.') + self.exit(fail=True) + + +def main(): + DecortSDNLogicalPort().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_sdn_logical_port_address.py b/library/decort_sdn_logical_port_address.py new file mode 100644 index 0000000..ca25676 --- /dev/null +++ b/library/decort_sdn_logical_port_address.py @@ -0,0 +1,206 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_logical_port_address + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests + + +class DecortSDNLogicalPortAddress(DecortController): + logical_port_id: str | None = None + _logical_port_info: dict[str, Any] | None = None + + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + discovered=dict( + type='bool', + ), + ip_addr=dict( + type='str', + required=True, + ), + ip_type=dict( + type='str', + choices=['IPv4', 'IPv6'], + ), + logical_port_id=dict( + type='str', + required=True, + ), + mac=dict( + type='str', + ), + primary=dict( + type='bool', + ), + state=dict( + type='str', + choices=['present', 'absent'], + ), + ), + supports_check_mode=True, + ) + + def check_amodule_args_for_create(self): + check_errors = False + + if self.aparams['ip_type'] is None: + check_errors = True + self.message( + 'Check for parameter "ip_type" failed: ' + 'ip_type must be specified when creating a new address.' + ) + + if self.aparams['mac'] is None: + check_errors = True + self.message( + 'Check for parameter "mac" failed: ' + 'mac must be specified when creating a new address.' + ) + + if check_errors: + self.exit(fail=True) + + @property + def logical_port_info(self) -> dict[str, Any]: + if self._logical_port_info is None: + logical_port_info = self.logical_port_get() + if logical_port_info is None: + raise TypeError + self._logical_port_info = logical_port_info + return self._logical_port_info + + def find_address(self) -> dict[str, Any] | None: + addresses = self.logical_port_info.get('bindings', {}).get( + 'logical_port_addresses', [] + ) + for addr in addresses: + if addr.get('ip') == self.aparams['ip_addr']: + return addr + return None + + def port_payload(self) -> dict: + info = self.logical_port_info + return { + 'logical_port_id': self.logical_port_id, + 'version_id': info['version_id'], + 'adapter_mac': info['adapter_mac'], + 'description': info['description'], + 'display_name': info['display_name'], + 'enabled': info['enabled'], + 'hypervisor': info['hypervisor'], + 'labels': info.get('labels'), + 'port_security': info['bindings']['port_security'], + 'segment_id': info['bindings']['segment_id'], + } + + def logical_port_update(self, payload: dict): + response = self.decort_api_call( + arg_req_function=requests.put, + arg_api_name='/restmachine/sdn/logical_port/update', + arg_json_body=payload, + accept_json_response=True, + ) + self._logical_port_info = response.json() + self.set_changed() + + @DecortController.waypoint + def logical_port_get(self) -> dict[str, Any]: + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/logical_port/get', + arg_params={'logical_port_id': self.logical_port_id}, + accept_json_response=True, + not_fail_codes=[404], + ) + if response.status_code == 404: + self.message( + self.MESSAGES.obj_not_found( + obj='logical port', + id=self.logical_port_id, + ) + ) + self.exit(fail=True) + return response.json() + + @DecortController.waypoint + @DecortController.checkmode + def address_add(self): + payload = self.port_payload() + address_data: dict[str, Any] = {'ip': self.aparams['ip_addr']} + for param_name, api_param_name in ( + ('ip_type', 'ip_type'), + ('discovered', 'is_discovered'), + ('primary', 'is_primary'), + ('mac', 'mac'), + ): + if self.aparams[param_name] is not None: + address_data[api_param_name] = self.aparams[param_name] + payload['add_addresses'] = [address_data] + self.logical_port_update(payload) + + @DecortController.waypoint + @DecortController.checkmode + def address_remove(self, address_id: str): + payload = self.port_payload() + payload['remove_addresses'] = [address_id] + self.logical_port_update(payload) + self.message( + self.MESSAGES.obj_deleted( + obj='logical port address', + id=self.aparams['ip_addr'], + ) + ) + + def run(self): + self.logical_port_id = self.aparams['logical_port_id'] + + existing_addr = self.find_address() + + if existing_addr: + if self.aparams['state'] == 'absent': + self.address_remove(existing_addr['id']) + else: + state = self.aparams['state'] + if state is None: + state = 'present' + self.message( + msg=self.MESSAGES.default_value_used( + param_name='state', + default_value=state, + ), + warning=True, + ) + if state == 'absent': + self.message( + self.MESSAGES.obj_not_found( + obj='logical port address', + id=self.aparams['ip_addr'], + ) + ) + else: + self.check_amodule_args_for_create() + self.address_add() + + self.facts = self.find_address() or {} + self.exit() + + +def main(): + DecortSDNLogicalPortAddress().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_sdn_logical_port_list.py b/library/decort_sdn_logical_port_list.py new file mode 100644 index 0000000..5dc817b --- /dev/null +++ b/library/decort_sdn_logical_port_list.py @@ -0,0 +1,171 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_logical_port_list + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests + + +class DecortSDNLogicalPortList(DecortController): + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + filter=dict( + type='dict', + apply_defaults=True, + options=dict( + access_group_id=dict( + type='str', + ), + adapter_mac=dict( + type='str', + ), + address_detection=dict( + type='bool', + ), + created_from=dict( + type='str', + ), + created_to=dict( + type='str', + ), + display_name=dict( + type='str', + ), + enabled=dict( + type='bool', + ), + external_network_id=dict( + type='str', + ), + hypervisor=dict( + type='str', + ), + hypervisor_display_name=dict( + type='str', + ), + hypervisor_status=dict( + type='str', + choices=['Up', 'Warning', 'Error'], + ), + live_migration_target_hv=dict( + type='str', + ), + operation_status=dict( + type='str', + choices=[ + 'Idle', + 'SynchronizingAtCore', + 'SynchronizingAtOVN', + 'Synchronized', + 'NoHypervisorAtOVN', + 'FailedAtCore', + 'TemporaryFailedAtCore', + ], + ), + port_security=dict( + type='bool', + ), + segment_display_name=dict( + type='str', + ), + segment_id=dict( + type='str', + ), + unique_identifier=dict( + type='str', + ), + ), + ), + pagination=dict( + type='dict', + apply_defaults=True, + options=dict( + number=dict( + type='int', + default=1, + ), + size=dict( + type='int', + default=50, + ), + ), + ), + sorting=dict( + type='dict', + options=dict( + asc=dict( + type='bool', + default=True, + ), + field=dict( + type='str', + choices=[ + 'created_at', + 'deleted_at', + 'display_name', + 'hypervisor', + 'hypervisor_display_name', + 'port_security', + 'primary_address', + 'segment_display_name', + 'segment_id', + 'updated_at', + ], + required=True, + ), + ), + ), + ), + supports_check_mode=True, + ) + + def run(self): + self.get_info() + self.exit() + + def get_info(self): + api_params: dict[str, Any] = dict() + + aparam_filter: dict[str, Any] = self.aparams['filter'] + for field, value in aparam_filter.items(): + if value is not None: + api_params[field] = value + + aparam_pagination: dict[str, Any] = self.aparams['pagination'] + api_params['page'] = aparam_pagination['number'] + api_params['per_page'] = aparam_pagination['size'] + + aparam_sorting: dict[str, Any] | None = self.aparams['sorting'] + if aparam_sorting: + api_params['sort_by'] = aparam_sorting['field'] + api_params['sort_order'] = ( + 'asc' if aparam_sorting['asc'] else 'desc' + ) + + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/logical_port/list', + arg_params=api_params, + accept_json_response=True, + ) + self.facts = response.json() + + +def main(): + DecortSDNLogicalPortList().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_sdn_network_object_group.py b/library/decort_sdn_network_object_group.py new file mode 100644 index 0000000..decc59f --- /dev/null +++ b/library/decort_sdn_network_object_group.py @@ -0,0 +1,299 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_network_object_group + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests + + +class DecortSDNNetworkObjectGroup(DecortController): + REQUIRED_FIELDS = ( + 'access_group_id', + 'description', + 'name', + ) + + object_group_id: str | None = None + _object_group_info: dict[str, Any] | None = None + + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + access_group_id=dict( + type='str', + ), + description=dict( + type='str', + ), + no_addresses=dict( + type='bool', + ), + no_logical_ports=dict( + type='bool', + ), + name=dict( + type='str', + ), + object_group_id=dict( + type='str', + ), + version_id=dict( + type='int', + ), + ), + supports_check_mode=True, + ) + + @property + def object_group_info(self) -> dict[str, Any]: + if self._object_group_info is None: + if not isinstance(self.object_group_id, str): + raise TypeError + object_group_info = self.network_object_group_get() + if object_group_info is None: + raise TypeError + self._object_group_info = object_group_info + return self._object_group_info + + def run(self): + if self.aparams['object_group_id']: + self.object_group_id = self.aparams['object_group_id'] + elif self.aparams['name']: + object_group_info = self.network_object_group_find( + name=self.aparams['name'] + ) + if object_group_info: + self.object_group_id = object_group_info['id'] + + if self.object_group_id is None: + self.check_amodule_args_for_create() + self.network_object_group_create() + else: + self.check_amodule_args_for_update() + self.network_object_group_update() + + if self.aparams['no_logical_ports']: + self.network_object_group_detach_logical_ports() + + self.facts = self.network_object_group_get() + self.exit() + + def check_amodule_args_for_create(self): + check_errors = False + + for field in self.REQUIRED_FIELDS: + if self.aparams[field] is None: + check_errors = True + self.message( + f'Check for parameter "{field}" failed: ' + f'"{field}" is required when creating an object group.' + ) + + if check_errors: + self.exit(fail=True) + + def check_amodule_args_for_update(self): + check_errors = False + version_id = self.aparams['version_id'] + if ( + version_id is not None + and version_id != self.object_group_info['version_id'] + ): + check_errors = True + self.message( + 'Check for parameters "version_id" failed: ' + 'object group version mismatch: ' + f'given version: {version_id}, ' + f'current version: {self.object_group_info["version_id"]}.' + ) + + if check_errors: + self.exit(fail=True) + + @DecortController.waypoint + def network_object_group_get(self) -> dict[str, Any] | None: + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/network_object_group/get', + arg_params={'object_group_id': self.object_group_id}, + not_fail_codes=[404], + accept_json_response=True, + ) + if response.status_code == 404: + self.message( + f'Network object group with id "{self.object_group_id}" ' + 'not found.' + ) + self.exit(fail=True) + return response.json() + + @DecortController.waypoint + @DecortController.checkmode + def network_object_group_find(self, name: str) -> dict[str, Any] | None: + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/network_object_group/list', + arg_params={'name': name}, + accept_json_response=True, + ) + return response.json()[0] if response.json() else None + + @DecortController.waypoint + @DecortController.checkmode + def network_object_group_create(self): + payload = dict() + for field in self.REQUIRED_FIELDS: + value = self.aparams[field] + if value is not None: + payload[field] = value + + response = self.decort_api_call( + arg_req_function=requests.post, + arg_api_name='/restmachine/sdn/network_object_group/create', + arg_json_body=payload, + accept_json_response=True, + ) + self._object_group_info = response.json() + self.object_group_id = response.json()['id'] + self.set_changed() + + @DecortController.waypoint + @DecortController.checkmode + def network_object_group_update(self): + need_update = False + + remove_addresses = False + if self.aparams['no_addresses']: + current_addresses = self.object_group_info.get('addresses') + if isinstance(current_addresses, list) and current_addresses: + remove_addresses = True + need_update = True + + for field in self.REQUIRED_FIELDS: + value = self.aparams[field] + if value is None: + continue + current_value = self.object_group_info.get(field) + if isinstance(value, list) and isinstance(current_value, list): + if len(value) != len(current_value): + need_update = True + break + for index, expected_item in enumerate(value): + current_item = current_value[index] + if ( + isinstance(expected_item, dict) + and isinstance(current_item, dict) + ): + if any( + current_item.get(key) != expected_value + for key, expected_value in expected_item.items() + ): + need_update = True + break + continue + if expected_item != current_item: + need_update = True + break + if need_update: + break + continue + if value != current_value: + need_update = True + break + + if not need_update: + return + + payload = { + 'object_group_id': self.object_group_id, + 'version_id': ( + self.aparams['version_id'] + or self.object_group_info['version_id'] + ), + 'access_group_id': ( + self.aparams['access_group_id'] + or self.object_group_info['access_group_id'] + ), + 'description': ( + self.aparams['description'] + or self.object_group_info['description'] + ), + 'name': ( + self.aparams['name'] + or self.object_group_info['name'] + ), + } + if remove_addresses: + payload['addresses'] = [] + + response = self.decort_api_call( + arg_req_function=requests.put, + arg_api_name='/restmachine/sdn/network_object_group/update', + arg_json_body=payload, + accept_json_response=True, + ) + self._object_group_info = response.json() + self.set_changed() + + @DecortController.waypoint + @DecortController.checkmode + def network_object_group_detach_logical_ports(self): + logical_ports = self.object_group_info.get('logical_ports') + if not isinstance(logical_ports, list) or not logical_ports: + return + + port_bindings = [] + for logical_port in logical_ports: + if ( + isinstance(logical_port, dict) + and logical_port.get('id') is not None + and logical_port.get('version_id') is not None + ): + port_bindings.append( + { + 'port_id': logical_port['id'], + 'port_version': logical_port['version_id'], + } + ) + + if not port_bindings: + return + + self.decort_api_call( + arg_req_function=requests.post, + arg_api_name='/restmachine/sdn/network_object_group/detach_logical_ports', # noqa: E501 + arg_json_body={ + 'access_group_id': ( + self.aparams['access_group_id'] + or self.object_group_info['access_group_id'] + ), + 'object_group_id': self.object_group_id, + 'version_id': ( + self.aparams['version_id'] + or self.object_group_info['version_id'] + ), + 'port_bindings': port_bindings, + }, + accept_json_response=True, + ) + self._object_group_info = None + self.set_changed() + + +def main(): + DecortSDNNetworkObjectGroup().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_sdn_network_object_group_ip_range.py b/library/decort_sdn_network_object_group_ip_range.py new file mode 100644 index 0000000..3f98412 --- /dev/null +++ b/library/decort_sdn_network_object_group_ip_range.py @@ -0,0 +1,209 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_network_object_group_ip_range + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests + + +class DecortSDNNetworkObjectGroupIPRange(DecortController): + object_group_id: str | None = None + _object_group_info: dict[str, Any] | None = None + + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + object_group_id=dict( + type='str', + required=True, + ), + ip_addr_range=dict( + type='dict', + required=True, + options=dict( + start=dict( + type='str', + required=True, + ), + end=dict( + type='str', + ), + ), + ), + ip_proto=dict( + type='str', + choices=['IPv4', 'IPv6'], + ), + net_prefix=dict( + type='str', + ), + state=dict( + type='str', + required=True, + choices=['present', 'absent'], + ), + ), + supports_check_mode=True, + required_if=[('state', 'present', ['ip_proto'])], + ) + + @property + def object_group_info(self) -> dict[str, Any]: + if self._object_group_info is None: + object_group_info = self.network_object_group_get() + if object_group_info is None: + raise TypeError + self._object_group_info = object_group_info + return self._object_group_info + + def find_ip_range(self) -> dict | None: + ip_addr = self.aparams['ip_addr_range']['start'] + ip_proto = self.aparams['ip_proto'] + for addr in self.object_group_info.get('addresses') or []: + if addr.get('ip_addr') != ip_addr: + continue + if ( + ip_proto is not None + and addr.get('net_address_type') != ip_proto + ): + continue + return addr + return None + + def group_payload(self) -> dict: + return { + 'object_group_id': self.object_group_id, + 'version_id': self.object_group_info['version_id'], + 'access_group_id': self.object_group_info['access_group_id'], + 'description': self.object_group_info['description'], + 'name': self.object_group_info['name'], + } + + def ip_range_add(self): + current_addresses = self.object_group_info.get('addresses') or [] + ip_addr_range = self.aparams['ip_addr_range'] + ip_proto = self.aparams['ip_proto'] + + ip_range_data = {'ip_addr': ip_addr_range['start']} + if ip_proto is not None: + ip_range_data['net_address_type'] = ip_proto + if ip_addr_range['end'] is not None: + ip_range_data['ip_addr_range_end'] = ip_addr_range['end'] + if self.aparams['net_prefix'] is not None: + ip_range_data['ip_prefix'] = self.aparams['net_prefix'] + + for index, addr in enumerate(current_addresses): + if addr.get('ip_addr') != ip_range_data['ip_addr']: + continue + if ( + ip_proto is not None + and addr.get('net_address_type') != ip_proto + ): + continue + if not any( + addr.get(field) != value + for field, value in ip_range_data.items() + if field in addr + ): + return + updated_addresses = list(current_addresses) + updated_addresses[index] = {**addr, **ip_range_data} + self.network_object_group_update_addresses(updated_addresses) + return + + self.network_object_group_update_addresses( + list(current_addresses) + [ip_range_data] + ) + + def ip_range_remove(self): + current_addresses = self.object_group_info.get('addresses') or [] + ip_addr = self.aparams['ip_addr_range']['start'] + ip_proto = self.aparams['ip_proto'] + + addresses_to_keep = [ + addr for addr in current_addresses + if addr.get('ip_addr') != ip_addr + or ( + ip_proto is not None + and addr.get('net_address_type') != ip_proto + ) + ] + if len(addresses_to_keep) == len(current_addresses): + self.message( + self.MESSAGES.obj_not_found( + obj='ip_range', + id=ip_addr, + ) + ) + return + + self.network_object_group_update_addresses(addresses_to_keep) + self.message( + self.MESSAGES.obj_deleted( + obj='ip_range', + id=ip_addr, + ) + ) + + @DecortController.waypoint + def network_object_group_get(self) -> dict[str, Any]: + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/network_object_group/get', + arg_params={'object_group_id': self.object_group_id}, + not_fail_codes=[404], + accept_json_response=True, + ) + if response.status_code == 404: + self.message( + self.MESSAGES.obj_not_found( + obj='network object group', + id=self.object_group_id, + ) + ) + self.exit(fail=True) + return response.json() + + @DecortController.waypoint + @DecortController.checkmode + def network_object_group_update_addresses(self, addresses: list): + payload = self.group_payload() + payload['addresses'] = addresses + response = self.decort_api_call( + arg_req_function=requests.put, + arg_api_name='/restmachine/sdn/network_object_group/update', + arg_json_body=payload, + accept_json_response=True, + ) + self._object_group_info = response.json() + self.set_changed() + + def run(self): + self.object_group_id = self.aparams['object_group_id'] + + if self.aparams['state'] == 'present': + self.ip_range_add() + else: + self.ip_range_remove() + + self.facts = self.find_ip_range() or {} + self.exit() + + +def main(): + DecortSDNNetworkObjectGroupIPRange().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_sdn_network_object_group_logical_port.py b/library/decort_sdn_network_object_group_logical_port.py new file mode 100644 index 0000000..2c1037a --- /dev/null +++ b/library/decort_sdn_network_object_group_logical_port.py @@ -0,0 +1,245 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_network_object_group_logical_port + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests + + +class DecortSDNNetworkObjectGroupLogicalPort(DecortController): + object_group_id: str + _object_group_info: dict[str, Any] | None = None + object_group_version: int | None = None + logical_port_id: str + logical_port_version: int | None = None + + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + access_group_id=dict( + type='str', + required=True, + ), + object_group_id=dict( + type='str', + required=True, + ), + logical_port_id=dict( + type='str', + required=True, + ), + logical_port_version=dict( + type='int', + ), + version_id=dict( + type='int', + ), + state=dict( + type='str', + choices=['present', 'absent'], + ), + ), + supports_check_mode=True, + ) + + @property + def object_group_info(self) -> dict[str, Any]: + if self._object_group_info is None: + if not isinstance(self.object_group_id, str): + raise TypeError + info = self.network_object_group_get() + if info is None: + raise TypeError + self._object_group_info = info + return self._object_group_info + + def run(self): + self.object_group_id = self.aparams['object_group_id'] + self.logical_port_id = self.aparams['logical_port_id'] + + is_attached = self.is_logical_port_attached() + state = self.aparams['state'] + self.object_group_version = self.get_object_group_version() + self.logical_port_version = self.get_port_version( + is_attached=is_attached + ) + + if state == 'present': + if not is_attached: + self.network_object_group_logical_port_attach() + elif state == 'absent': + if is_attached: + self.network_object_group_logical_port_detach() + + self.facts = {} + self.exit() + + def get_object_group_version(self) -> int: + provided_version: int | None = self.aparams['version_id'] + current_version: int = self.object_group_info['version_id'] + + if ( + provided_version is not None + and provided_version != current_version + ): + self.message( + 'Check for parameters "version_id" failed: ' + 'object group version mismatch: ' + f'given version: {provided_version}, ' + f'current version: {current_version}.' + ) + self.exit(fail=True) + + return provided_version or current_version + + @DecortController.waypoint + def network_object_group_get(self) -> dict[str, Any] | None: + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/network_object_group/get', + arg_params={'object_group_id': self.object_group_id}, + not_fail_codes=[404], + accept_json_response=True, + ) + if response.status_code == 404: + self.message( + self.MESSAGES.obj_not_found( + obj='network object group', + id=self.object_group_id, + ) + ) + self.exit(fail=True) + return response.json() + + @DecortController.waypoint + def logical_port_get(self, logical_port_id: str) -> dict[str, Any]: + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/logical_port/get', + arg_params={'logical_port_id': logical_port_id}, + not_fail_codes=[404], + accept_json_response=True, + ) + if response.status_code == 404: + self.message( + self.MESSAGES.obj_not_found( + obj='logical port', + id=logical_port_id, + ) + ) + self.exit(fail=True) + return response.json() + + def is_logical_port_attached(self) -> bool: + logical_ports = self.object_group_info.get('logical_ports') or [] + for lp in logical_ports: + if lp['id'] == self.logical_port_id: + return True + return False + + def get_port_version_from_object_group(self) -> int | None: + logical_ports = self.object_group_info.get('logical_ports') or [] + for lp in logical_ports: + if lp['id'] != self.logical_port_id: + continue + return lp['version_id'] + return None + + def get_port_version(self, is_attached: bool) -> int: + provided_version = self.aparams['logical_port_version'] + + current_version = None + if is_attached: + current_version = self.get_port_version_from_object_group() + + if current_version is None: + info = self.logical_port_get(logical_port_id=self.logical_port_id) + current_version = info['version_id'] + + if ( + provided_version is not None + and provided_version != current_version + ): + self.message( + 'Check for parameter "logical_port_version" failed: ' + 'logical port version mismatch: ' + f'given version: {provided_version}, ' + f'current version: {current_version}.' + ) + self.exit(fail=True) + + return provided_version or current_version + + @DecortController.waypoint + @DecortController.checkmode + def network_object_group_logical_port_attach(self): + self.decort_api_call( + arg_req_function=requests.post, + arg_api_name='/restmachine/sdn/network_object_group/attach_logical_ports', # noqa: E501 + arg_json_body={ + 'object_group_id': self.object_group_id, + 'access_group_id': self.aparams['access_group_id'], + 'version_id': self.object_group_version, + 'port_bindings': [ + { + 'port_id': self.logical_port_id, + 'port_version': self.logical_port_version, + } + ], + }, + accept_json_response=True, + ) + self.message( + msg=( + f'Logical port ID {self.logical_port_id} attached to object ' + f'group ID {self.object_group_id}.' + ) + ) + self.set_changed() + + @DecortController.waypoint + @DecortController.checkmode + def network_object_group_logical_port_detach(self): + self.decort_api_call( + arg_req_function=requests.post, + arg_api_name='/restmachine/sdn/network_object_group/detach_logical_ports', # noqa: E501 + arg_json_body={ + 'object_group_id': self.object_group_id, + 'access_group_id': self.aparams['access_group_id'], + 'version_id': self.object_group_version, + 'port_bindings': [ + { + 'port_id': self.logical_port_id, + 'port_version': self.logical_port_version, + } + ], + }, + accept_json_response=True, + ) + self.message( + msg=( + f'Logical port ID {self.logical_port_id} detached from object ' + f'group ID {self.object_group_id}.' + ) + ) + self.set_changed() + + +def main(): + DecortSDNNetworkObjectGroupLogicalPort().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_sdn_network_object_group_mac.py b/library/decort_sdn_network_object_group_mac.py new file mode 100644 index 0000000..4810b50 --- /dev/null +++ b/library/decort_sdn_network_object_group_mac.py @@ -0,0 +1,166 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_network_object_group_mac + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests + + +class DecortSDNNetworkObjectGroupMAC(DecortController): + network_object_group_id: str | None = None + _network_object_group_info: dict[str, Any] | None = None + + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + network_object_group_id=dict( + type='str', + required=True, + ), + mac=dict( + type='str', + required=True, + ), + state=dict( + type='str', + required=True, + choices=['present', 'absent'], + ), + ), + supports_check_mode=True, + ) + + @property + def network_network_object_group_info(self) -> dict[str, Any]: + if self._network_object_group_info is None: + network_network_object_group_info = self.network_object_group_get() + if network_network_object_group_info is None: + raise TypeError + self._network_object_group_info = network_network_object_group_info + return self._network_object_group_info + + def find_mac(self) -> dict | None: + mac = self.aparams['mac'] + for addr in self.network_network_object_group_info.get('addresses') or []: # noqa: E501 + if addr.get('mac_addr') == mac: + return addr + return None + + def group_payload(self) -> dict: + return { + 'object_group_id': self.network_object_group_id, + 'version_id': self.network_network_object_group_info['version_id'], + 'access_group_id': self.network_network_object_group_info['access_group_id'], # noqa: E501 + 'description': self.network_network_object_group_info['description'], # noqa: E501 + 'name': self.network_network_object_group_info['name'], + } + + def mac_add(self): + current_addresses = self.network_network_object_group_info.get('addresses') or [] # noqa: E501 + mac = self.aparams['mac'] + + for addr in current_addresses: + if addr.get('mac_addr') == mac: + return + + mac_data = { + 'mac_addr': mac, + 'net_address_type': 'MAC', + } + self.network_object_group_update_addresses( + list(current_addresses) + [mac_data] + ) + + def mac_remove(self): + current_addresses = self.network_network_object_group_info.get('addresses') or [] # noqa: E501 + mac = self.aparams['mac'] + + addresses_to_keep = [ + addr for addr in current_addresses + if addr.get('mac_addr') != mac + ] + if len(addresses_to_keep) == len(current_addresses): + self.message( + self.MESSAGES.obj_not_found( + obj='mac', + id=mac, + ) + ) + return + + self.network_object_group_update_addresses(addresses_to_keep) + self.message( + self.MESSAGES.obj_deleted( + obj='mac', + id=mac, + ) + ) + + @DecortController.waypoint + def network_object_group_get(self) -> dict[str, Any]: + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/network_object_group/get', + arg_params={'object_group_id': self.network_object_group_id}, + not_fail_codes=[404], + accept_json_response=True, + ) + if response.status_code == 404: + self.message( + self.MESSAGES.obj_not_found( + obj='network object group', + id=self.network_object_group_id, + ) + ) + self.exit(fail=True) + return response.json() + + @DecortController.waypoint + @DecortController.checkmode + def network_object_group_update_addresses(self, addresses: list): + payload = self.group_payload() + payload['addresses'] = addresses + response = self.decort_api_call( + arg_req_function=requests.put, + arg_api_name='/restmachine/sdn/network_object_group/update', + arg_json_body=payload, + accept_json_response=True, + ) + self._network_object_group_info = response.json() + self.set_changed() + + def package_facts(self) -> dict: + facts = self.find_mac() or {} + if 'mac_addr' in facts: + facts['mac'] = facts.pop('mac_addr') + return facts + + def run(self): + self.network_object_group_id = self.aparams['network_object_group_id'] + + if self.aparams['state'] == 'present': + self.mac_add() + else: + self.mac_remove() + + self.facts = self.package_facts() + self.exit() + + +def main(): + DecortSDNNetworkObjectGroupMAC().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_sdn_segment.py b/library/decort_sdn_segment.py new file mode 100644 index 0000000..fdaa583 --- /dev/null +++ b/library/decort_sdn_segment.py @@ -0,0 +1,357 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_segment + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests + + +class DecortSDNSegment(DecortController): + REQUIRED_FIELDS = ( + 'access_group_id', + 'description', + 'dhcp_v4', + 'dhcp_v6', + 'display_name', + 'subnet_v4', + 'subnet_v6', + 'type', + ) + + segment_id: str | None = None + _segment_info: dict[str, Any] | None = None + need_final_get: bool = True + + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + access_group_id=dict( + type='str', + ), + description=dict( + type='str', + ), + dhcp_v4=dict( + type='dict', + ), + dhcp_v6=dict( + type='dict', + ), + display_name=dict( + type='str', + ), + segment_id=dict( + type='str', + ), + state=dict( + type='str', + choices=[ + 'present', + 'enabled', + 'disabled', + 'absent', + 'absent_force', + ], + ), + subnet_v4=dict( + type='str', + ), + subnet_v6=dict( + type='str', + ), + type=dict( + type='str', + choices=['User', 'ExtNet'], + ), + version_id=dict( + type='int', + ), + ), + supports_check_mode=True, + ) + + @property + def segment_info(self) -> dict[str, Any]: + if self._segment_info is None: + if not isinstance(self.segment_id, str): + raise TypeError + segment_info = self.segment_get() + if segment_info is None: + raise TypeError + self._segment_info = segment_info + return self._segment_info + + def run(self): + state = self.aparams['state'] + + if self.aparams['segment_id']: + self.segment_id = self.aparams['segment_id'] + self._segment_info = self.segment_get(fail_if_not_found=False) + elif self.aparams['display_name']: + segment_info = self.segment_find( + display_name=self.aparams['display_name'] + ) + self._segment_info = segment_info + if segment_info: + self.segment_id = segment_info['id'] + + if state in ('absent', 'absent_force'): + if self._segment_info is None: + self.exit() + if self.segment_id: + self.segment_delete( + force=state == 'absent_force', + ) + else: + self.need_final_get = False + elif state == 'present': + if self.segment_id: + self.check_amodule_args_for_update() + self.segment_update() + else: + self.check_amodule_args_for_create() + self.segment_create() + elif state in ('enabled', 'disabled'): + if not self.segment_id: + self.message( + 'Check for parameter "state" failed: state values ' + '"enabled"/"disabled" can only be applied to an existing ' + 'segment.' + ) + self.check_amodule_args_for_update() + self.segment_update( + desired_enabled=(state == 'enabled'), + ) + + if self.need_final_get: + self.facts = self.segment_get() + self.exit() + + def check_amodule_args_for_create(self): + check_errors = False + + if ( + self.aparams['subnet_v4'] is None + and self.aparams['subnet_v6'] is None + ): + check_errors = True + self.message( + 'Check for parameters "subnet_v4/subnet_v6" failed: at ' + 'least one of these parameters must be specified when ' + 'creating a segment.' + ) + + if check_errors: + self.exit(fail=True) + + def check_amodule_args_for_update(self): + check_errors = False + + if ( + self.aparams['version_id'] is not None + and self.aparams['version_id'] != self.segment_info['version_id'] + ): + check_errors = True + self.message( + 'Check for parameters "version_id" failed: ' + 'segment version mismatch: ' + f'given version: {self.aparams['version_id']}, ' + f'current version: {self.segment_info['version_id']}.' + ) + + if check_errors: + self.exit(fail=True) + + @DecortController.waypoint + @DecortController.checkmode + def segment_get( + self, + access_group_id: str | None = None, + fail_if_not_found=True, + ) -> dict[str, Any] | None: + params = {'segment_id': self.segment_id} + if access_group_id is not None: + params['access_group_id'] = access_group_id + + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/segment/get', + arg_params=params, + not_fail_codes=[404], + accept_json_response=True, + ) + if response.status_code == 404: + if fail_if_not_found: + self.message( + self.MESSAGES.obj_not_found( + obj='segment', + id=self.segment_id, + ) + ) + self.exit(fail=True) + else: + return None + return response.json() + + @DecortController.waypoint + @DecortController.checkmode + def segment_find(self, display_name: str) -> dict[str, Any] | None: + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/segment/list', + arg_params={ + 'display_name': display_name, + }, + accept_json_response=True, + ) + return response.json()[0] if response.json() else None + + @DecortController.waypoint + @DecortController.checkmode + def segment_create(self): + payload = dict() + for field in self.REQUIRED_FIELDS: + value = self.aparams[field] + if value is not None: + payload[field] = value + payload['enabled'] = True + response = self.decort_api_call( + arg_req_function=requests.post, + arg_api_name='/restmachine/sdn/segment/create', + arg_json_body=payload, + accept_json_response=True, + ) + self._segment_info = response.json() + self.segment_id = response.json()['id'] + self.set_changed() + + @DecortController.waypoint + @DecortController.checkmode + def segment_update( + self, + desired_enabled: bool | None = None, + ): + need_update = False + + for field in self.REQUIRED_FIELDS: + value = self.aparams[field] + if value is None: + continue + current_value = self.segment_info.get(field) + if isinstance(value, dict) and isinstance(current_value, dict): + if any( + current_value.get(key) != expected_value + for key, expected_value in value.items() + ): + need_update = True + break + continue + if value != current_value: + need_update = True + break + + if ( + not need_update + and desired_enabled is not None + and desired_enabled != self.segment_info['enabled'] + ): + need_update = True + + if need_update: + payload = { + 'segment_id': self.segment_id, + 'version_id': ( + self.aparams['version_id'] + or self.segment_info['version_id'] + ), + 'access_group_id': ( + self.aparams['access_group_id'] + or self.segment_info['access_group_id'] + ), + 'description': ( + self.aparams['description'] + or self.segment_info['description'] + ), + 'dhcp_v4': ( + self.aparams['dhcp_v4'] or self.segment_info.get('dhcp_v4') + ), + 'dhcp_v6': ( + self.aparams['dhcp_v6'] or self.segment_info.get('dhcp_v6') + ), + 'display_name': ( + self.aparams['display_name'] + or self.segment_info['display_name'] + ), + 'enabled': ( + desired_enabled + if desired_enabled is not None + else self.segment_info['enabled'] + ), + 'subnet_v4': ( + self.aparams['subnet_v4'] + or self.segment_info.get('subnet_v4') + ), + 'subnet_v6': ( + self.aparams['subnet_v6'] + or self.segment_info.get('subnet_v6') + ), + 'type': ( + self.aparams['type'] or self.segment_info['type'] + ), + } + + self.decort_api_call( + arg_req_function=requests.put, + arg_api_name='/restmachine/sdn/segment/update', + arg_json_body=payload, + accept_json_response=True, + ) + self.set_changed() + + @DecortController.waypoint + @DecortController.checkmode + def segment_delete( + self, + force: bool = False, + ): + version_id = self.aparams['version_id'] + if version_id is None: + version_id = self.segment_info['version_id'] + payload = { + 'segment_id': self.segment_id, + 'version_id': version_id, + } + self.decort_api_call( + arg_req_function=requests.delete, + arg_api_name='/restmachine/sdn/segment/delete', + arg_params=payload, + ) + self.need_final_get = False + self.set_changed() + self.message( + self.MESSAGES.obj_deleted( + obj='segment', + id=self.segment_id, + permanently=force, + ) + ) + self.facts = {} + + +def main(): + DecortSDNSegment().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_sdn_segment_list.py b/library/decort_sdn_segment_list.py new file mode 100644 index 0000000..7203985 --- /dev/null +++ b/library/decort_sdn_segment_list.py @@ -0,0 +1,142 @@ +#!/usr/bin/python + +DOCUMENTATION = r''' +--- +module: decort_sdn_segment_list + +description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). # noqa: E501 +''' + +from typing import Any +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.decort_utils import DecortController +import requests + + +class DecortSDNSegmentList(DecortController): + def __init__(self): + super().__init__(AnsibleModule(**self.amodule_init_args)) + + @property + def amodule_init_args(self) -> dict: + return self.pack_amodule_init_args( + argument_spec=dict( + filter=dict( + type='dict', + apply_defaults=True, + options=dict( + enabled=dict( + type='bool', + ), + is_synced=dict( + type='bool', + ), + display_name=dict( + type='str', + ), + subnet=dict( + type='str', + ), + access_group_id=dict( + type='str', + ), + created_from=dict( + type='str', + ), + created_to=dict( + type='str', + ), + updated_from=dict( + type='str', + ), + updated_to=dict( + type='str', + ), + operation_status=dict( + type='str', + choices=[ + 'Idle', + 'SynchronizingAtCore', + 'SynchronizingAtOVN', + 'Synchronized', + 'NoHypervisorAtOVN', + 'FailedAtCore', + 'TemporaryFailedAtCore', + ], + ), + ), + ), + pagination=dict( + type='dict', + apply_defaults=True, + options=dict( + number=dict( + type='int', + default=1, + ), + size=dict( + type='int', + ), + ), + ), + sorting=dict( + type='dict', + options=dict( + asc=dict( + type='bool', + default=True, + ), + field=dict( + type='str', + choices=[ + 'display_name', + 'subnet', + 'created_at', + 'updated_at', + ], + required=True, + ), + ), + ), + ), + supports_check_mode=True, + ) + + def run(self): + self.get_info() + self.exit() + + def get_info(self): + api_params: dict[str, Any] = dict() + + aparam_filter: dict[str, Any] = self.aparams['filter'] + for field, value in aparam_filter.items(): + if value is not None: + api_params[field] = value + + aparam_pagination: dict[str, Any] = self.aparams['pagination'] + api_params['page'] = aparam_pagination['number'] + api_params['per_page'] = aparam_pagination['size'] + + aparam_sorting: dict[str, Any] | None = self.aparams['sorting'] + if aparam_sorting: + api_params['sort_by'] = aparam_sorting['field'] + api_params['sort_order'] = ( + 'asc' if aparam_sorting['asc'] else 'desc' + ) + + response = self.decort_api_call( + arg_req_function=requests.get, + arg_api_name='/restmachine/sdn/segment/list', + arg_params=api_params, + accept_json_response=True, + ) + self.facts = response.json() + + +def main(): + DecortSDNSegmentList().run() + + +if __name__ == '__main__': + main() diff --git a/library/decort_security_group.py b/library/decort_security_group.py index 8b611a7..6a6b5bb 100644 --- a/library/decort_security_group.py +++ b/library/decort_security_group.py @@ -136,7 +136,7 @@ class DecortSecurityGroup(DecortController): ) except sdk_exceptions.RequestException as e: if ( - e.orig_exception.response + e.orig_exception.response is not None and e.orig_exception.response.status_code == 404 ): self.message( diff --git a/library/decort_storage_policy.py b/library/decort_storage_policy.py index 0e188f8..69c1c24 100644 --- a/library/decort_storage_policy.py +++ b/library/decort_storage_policy.py @@ -42,7 +42,7 @@ class DecortStoragePolicy(DecortController): ) except sdk_exceptions.RequestException as e: if ( - e.orig_exception.response + e.orig_exception.response is not None and e.orig_exception.response.status_code == 404 ): self.message( diff --git a/library/decort_trunk.py b/library/decort_trunk.py index 3a44a70..2313642 100644 --- a/library/decort_trunk.py +++ b/library/decort_trunk.py @@ -42,7 +42,7 @@ class DecortTrunk(DecortController): ) except sdk_exceptions.RequestException as e: if ( - e.orig_exception.response + e.orig_exception.response is not None and e.orig_exception.response.status_code == 404 ): self.message( diff --git a/library/decort_user.py b/library/decort_user.py index dfe23f9..061b4f8 100644 --- a/library/decort_user.py +++ b/library/decort_user.py @@ -10,6 +10,8 @@ description: See L(Module Documentation,https://repository.basistech.ru/BASIS/de from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.decort_utils import DecortController +from dynamix_sdk import exceptions as sdk_exceptions + class DecortUser(DecortController): def __init__(self): @@ -43,16 +45,35 @@ class DecortUser(DecortController): self.facts = self.usermanager_whoami_result self.id = self.facts['name'] - user_get = self.user_get(id=self.id) - for key in ['emailaddresses', 'data']: - self.facts[key] = user_get[key] + try: + user_model = self.api.cloudapi.user.get(user_name=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='user', + ) + ) + self.exit(fail=True) + raise e + + self.facts.update(user_model.model_dump()) if self.aparams['resource_consumption']: - self.facts.update(self.user_resource_consumption()) + self.facts.update( + self.api.ca.user.get_resource_consumption().model_dump() + ) + + # Delete duplicate self.facts['name'] + del self.facts['user_name'] if self.aparams['api_methods']: - self.facts['api_methods'] = self.user_api_methods(id=self.id) - + self.facts['api_methods'] = ( + self.api.cloudapi.user.api_list(user_name=self.id).model_dump() + ) search_string = self.aparams['objects_search'] if search_string: self.facts['objects_search'] = self.user_objects_search( diff --git a/library/decort_vins.py b/library/decort_vins.py index 9173cef..d3cf6d9 100644 --- a/library/decort_vins.py +++ b/library/decort_vins.py @@ -20,7 +20,6 @@ class decort_vins(DecortController): self.vins_id = 0 self.vins_level = "" # "ID" if specified by ID, "RG" - at resource group, "ACC" - at account level - vins_facts = None # will hold ViNS facts validated_rg_id = 0 rg_facts = None # will hold RG facts validated_acc_id = 0 @@ -28,15 +27,28 @@ class decort_vins(DecortController): if arg_amodule.params['vins_id']: # expect existing ViNS with the specified ID # This call to vins_find will abort the module if no ViNS with such ID is present - self.vins_id, self.vins_facts = self.vins_find(arg_amodule.params['vins_id'],check_state=False) + self.vins_id, self._vins_info = self.vins_find( + arg_amodule.params['vins_id'], + check_state=False, + ) if self.vins_id == 0: + if arg_amodule.params['state'] == 'absent': + self.exit() + else: + self.message( + self.MESSAGES.obj_not_found( + obj='VINS', + id=arg_amodule.params['vins_id'], + ) + ) + self.exit(fail=True) + if self._vins_info is None: self.result['failed'] = True self.result['msg'] = "Specified ViNS ID {} not found.".format(arg_amodule.params['vins_id']) self.amodule.fail_json(**self.result) self.vins_level = "ID" - #raise Exception(self.vins_facts) - validated_acc_id = self.vins_facts['accountId'] - validated_rg_id = self.vins_facts['rgId'] + validated_acc_id = self._vins_info.account_id + validated_rg_id = self._vins_info.rg_id elif arg_amodule.params['rg_id']: # expect ViNS @ RG level in the RG with specified ID @@ -44,14 +56,16 @@ class decort_vins(DecortController): # This call to rg_find will abort the module if no RG with such ID is present validated_rg_id, rg_facts = self.rg_find(0, # account ID set to 0 as we search for RG by RG ID arg_amodule.params['rg_id'], arg_rg_name="") - validated_acc_id = rg_facts['accountId'] - + validated_acc_id = rg_facts.account_id + # This call to vins_find may return vins_id=0 if no ViNS found - self.vins_id, self.vins_facts = self.vins_find(vins_id=0, vins_name=arg_amodule.params['vins_name'], - account_id=0, - rg_id=arg_amodule.params['rg_id'], - rg_facts=rg_facts, - check_state=False) + self.vins_id, self._vins_info = self.vins_find( + vins_id=0, + vins_name=arg_amodule.params['vins_name'], + account_id=0, + rg_id=arg_amodule.params['rg_id'], + check_state=False, + ) # TODO: add checks and setup ViNS presence flags accordingly pass elif arg_amodule.params['account_id'] or arg_amodule.params['account_name'] != "": @@ -66,27 +80,39 @@ class decort_vins(DecortController): # expect ViNS @ RG level in the RG with specified name under specified account # RG with the specified name must be present under the account, otherwise abort the module validated_rg_id, rg_facts = self.rg_find(validated_acc_id, 0, arg_amodule.params['rg_name']) - if (not validated_rg_id or - rg_facts['status'] in ["DESTROYING", "DESTROYED", "DELETING", "DELETED", "DISABLING", "ENABLING"]): + if (not validated_rg_id or rg_facts is None or + rg_facts.status in [ + sdk_types.ResourceGroupStatus.DESTROYING, + sdk_types.ResourceGroupStatus.DESTROYED, + sdk_types.ResourceGroupStatus.DELETED, + sdk_types.ResourceGroupStatus.DISABLING, + sdk_types.ResourceGroupStatus.ENABLING, + ] + ): self.result['failed'] = True self.result['msg'] = "RG name '{}' not found or has invalid state.".format(arg_amodule.params['rg_name']) self.amodule.fail_json(**self.result) # This call to vins_find may return vins_id=0 if no ViNS with this name found under specified RG - self.vins_id, self.vins_facts = self.vins_find(vins_id=0, vins_name=arg_amodule.params['vins_name'], - account_id=0, # set to 0, as we are looking for ViNS under RG - rg_id=validated_rg_id, - rg_facts=rg_facts, - check_state=False) + # (account_id) set to 0, as we are looking for ViNS under RG + self.vins_id, self._vins_info = self.vins_find( + vins_id=0, + vins_name=arg_amodule.params['vins_name'], + account_id=0, + rg_id=validated_rg_id, + check_state=False, + ) self.vins_level = "RG" # TODO: add checks and setup ViNS presence flags accordingly else: # At this point we know for sure that rg_name="" and rg_id=0 # So we expect ViNS @ account level # This call to vins_find may return vins_id=0 if no ViNS found - self.vins_id, self.vins_facts = self.vins_find(vins_id=0, vins_name=arg_amodule.params['vins_name'], - account_id=validated_acc_id, - rg_id=0, - rg_facts=rg_facts, - check_state=False) + self.vins_id, self._vins_info = self.vins_find( + vins_id=0, + vins_name=arg_amodule.params['vins_name'], + account_id=validated_acc_id, + rg_id=0, + check_state=False, + ) self.vins_level = "ACC" # TODO: add checks and setup ViNS presence flags accordingly else: @@ -106,73 +132,121 @@ class decort_vins(DecortController): self.rg_id = validated_rg_id self.acc_id = validated_acc_id - if self.vins_id and self.vins_facts['status'] != 'DESTROYED': + if ( + self._vins_info + and self._vins_info.status != sdk_types.VINSStatus.DESTROYED + ): self.check_amodule_args_for_change() else: self.check_amodule_args_for_create() - return + return def create(self): - self.vins_id = self.vins_provision(self.amodule.params['vins_name'], - self.acc_id, self.rg_id, - self.amodule.params['ipcidr'], - self.amodule.params['ext_net_id'], self.amodule.params['ext_ip_addr'], - self.amodule.params['description'], - zone_id=self.amodule.params['zone_id'], + security_group_mode = self.amodule.params['security_group_mode'] + if security_group_mode is None: + security_group_mode = False + self.message( + msg=self.MESSAGES.default_value_used( + param_name='security_group_mode', + default_value=security_group_mode + ), + warning=True, + ) + + self.vins_id = self.vins_provision( + vins_name=self.amodule.params['vins_name'], + account_id=self.acc_id, + rg_id=self.rg_id, + ipcidr=self.amodule.params['ipcidr'], + ext_net_id=self.amodule.params['ext_net_id'], + ext_ip_addr=self.amodule.params['ext_ip_addr'], + desc=self.amodule.params['description'], + zone_id=self.amodule.params['zone_id'], + security_group_mode=security_group_mode, ) - - if self.amodule.params['mgmtaddr'] or self.amodule.params['connect_to']: - _, self.vins_facts = self.vins_find(self.vins_id) + if self.vins_id: + self._vins_info = self._vins_get_by_id(vins_id=self.vins_id) if self.amodule.params['connect_to']: - self.vins_update_ifaces(self.vins_facts,self.amodule.params['connect_to'],) + self.vins_update_ifaces( + self.vins_info, + self.amodule.params['connect_to'], + ) if self.amodule.params['mgmtaddr']: - self.vins_update_mgmt(self.vins_facts,self.amodule.params['mgmtaddr']) - + self.vins_update_mgmt( + self.vins_info, + self.amodule.params['mgmtaddr'], + ) return - def action(self,d_state='',restore=False): - if restore == True: - self.vins_restore(arg_vins_id=self.vins_id) - self.vins_state(self.vins_facts, 'enabled') - self.vins_facts['status'] = "ENABLED" - self.vins_facts['VNFDev']['techStatus'] = "STARTED" - - self.vins_update_extnet(self.vins_facts, - self.amodule.params['ext_net_id'], - self.amodule.params['ext_ip_addr'], - ) - - if d_state == 'enabled' and self.vins_facts['status'] == "DISABLED": - self.vins_state(self.vins_facts, d_state) - self.vins_facts['status'] = "ENABLED" - self.vins_facts['VNFDev']['techStatus'] = "STARTED" + + def action(self, d_state='', restore=False): + if restore: + self.sdk_checkmode(self.api.cloudapi.vins.restore)( + vins_id=self.vins_info.id, + ) + self._vins_info = self._vins_get_by_id(vins_id=self.vins_id) + self.vins_state(self.vins_info, 'enabled') + + self._vins_info = self._vins_get_by_id(vins_id=self.vins_id) + + if ( + self.amodule.params['ext_net_id'] is not None + or self.amodule.params['ext_ip_addr'] is not None + ): + self.vins_update_extnet( + self.vins_info, + self.amodule.params['ext_net_id'], + self.amodule.params['ext_ip_addr'], + ) + + if ( + d_state == 'enabled' + and self.vins_info.status == sdk_types.VINSStatus.DISABLED + ): + self.vins_state(self.vins_info, d_state) + self._vins_info = self._vins_get_by_id(vins_id=self.vins_id) d_state = '' - if self.vins_facts['status'] == "ENABLED" and self.vins_facts['VNFDev']['techStatus'] == "STARTED": - self.vins_update_ifaces(self.vins_facts, - self.amodule.params['connect_to'], - ) + if ( + self.vins_info.status == sdk_types.VINSStatus.ENABLED + and self.vins_info.vnfdev.tech_status == ( + sdk_types.VNFDevTechStatus.STARTED + ) + ): + self.vins_update_ifaces( + self.vins_info, + self.amodule.params['connect_to'], + ) if self.result['changed']: - _, self.vins_facts = self.vins_find(self.vins_id) - self.vins_update_mgmt(self.vins_facts, - self.amodule.params['mgmtaddr'], - ) - + self._vins_info = self._vins_get_by_id(vins_id=self.vins_id) + + self.vins_update_mgmt( + self.vins_info, + self.amodule.params['mgmtaddr'], + ) + if d_state != '': - self.vins_state(self.vins_facts, d_state) - + self.vins_state(self.vins_info, d_state) + aparam_zone_id = self.aparams['zone_id'] - if aparam_zone_id is not None and aparam_zone_id != self.vins_facts['zoneId']: + if ( + aparam_zone_id is not None + and aparam_zone_id != self.vins_info.zone_id + ): self.vins_migrate_to_zone( - net_id=self.vins_id, + net_id=self.vins_info.id, zone_id=aparam_zone_id, ) return + def delete(self): - self.vins_delete(self.vins_id, self.amodule.params['permanently']) - self.vins_facts['status'] = 'DESTROYED' + self.sdk_checkmode(self.api.cloudapi.vins.delete)( + vins_id=self.vins_info.id, + permanently=self.amodule.params['permanently'], + ) return + def nop(self): """No operation (NOP) handler for ViNS management by decort_vins module. This function is intended to be called from the main switch construct of the module @@ -181,23 +255,27 @@ class decort_vins(DecortController): """ self.result['failed'] = False self.result['changed'] = False - if self.vins_id: - self.result['msg'] = ("No state change required for ViNS ID {} because of its " - "current status '{}'.").format(self.vins_id, self.vins_facts['status']) + if self._vins_info: + self.result['msg'] = ( + f'No state change required for ViNS ID {self._vins_info.id} ' + f'because of its "current status "{self._vins_info.status}".' + ) else: self.result['msg'] = ("No state change to '{}' can be done for " "non-existent ViNS instance.").format(self.amodule.params['state']) return + def error(self): self.result['failed'] = True self.result['changed'] = False - if self.vins_id: + if self._vins_info: self.result['failed'] = True self.result['changed'] = False - self.result['msg'] = ("Invalid target state '{}' requested for ViNS ID {} in the " - "current status '{}'").format(self.vins_id, - self.amodule.params['state'], - self.vins_facts['status']) + self.result['msg'] = ( + f'Invalid target state "{self.amodule.params['state']}" ' + f'requested for ViNS ID {self._vins_info.id} in the ' + f'current status "{self._vins_info.status}"' + ) else: self.result['failed'] = True self.result['changed'] = False @@ -205,6 +283,7 @@ class decort_vins(DecortController): "ViNS name '{}'").format(self.amodule.params['state'], self.amodule.params['vins_name']) return + def package_facts(self, arg_check_mode=False): """Package a dictionary of ViNS facts according to the decort_vins module specification. This dictionary will be returned to the upstream Ansible engine at the completion of @@ -222,40 +301,12 @@ class decort_vins(DecortController): # in check mode return immediately with the default values return ret_dict - if self.vins_facts is None: + if self._vins_info is None: # if void facts provided - change state value to ABSENT and return ret_dict['state'] = "ABSENT" return ret_dict - ret_dict['id'] = self.vins_facts['id'] - ret_dict['name'] = self.vins_facts['name'] - ret_dict['state'] = self.vins_facts['status'] - ret_dict['account_id'] = self.vins_facts['accountId'] - ret_dict['rg_id'] = self.vins_facts['rgId'] - ret_dict['int_net_addr'] = self.vins_facts['network'] - ret_dict['gid'] = self.vins_facts['gid'] - custom_interfaces = list(filter(lambda i: i['type']=="CUSTOM",self.vins_facts['VNFDev']['interfaces'])) - if custom_interfaces: - ret_dict['custom_net_addr'] = [] - for runner in custom_interfaces: - ret_dict['custom_net_addr'].append(runner['ipAddress']) - mgmt_interfaces = list(filter(lambda i: i['listenSsh'] and i['name']!="ens9",self.vins_facts['VNFDev']['interfaces'])) - if mgmt_interfaces: - ret_dict['ssh_ipaddr'] = [] - for runner in mgmt_interfaces: - ret_dict['ssh_ipaddr'].append(runner['ipAddress']) - ret_dict['ssh_password'] = self.vins_facts['VNFDev']['config']['mgmt']['password'] - ret_dict['ssh_port'] = 9022 - if self.vins_facts['vnfs'].get('GW'): - gw_config = self.vins_facts['vnfs']['GW']['config'] - ret_dict['ext_ip_addr'] = gw_config['ext_net_ip'] - ret_dict['ext_net_id'] = gw_config['ext_net_id'] - else: - ret_dict['ext_ip_addr'] = "" - ret_dict['ext_net_id'] = -1 - ret_dict['zone_id'] = self.vins_facts['zoneId'] - - return ret_dict + return self._vins_info.model_dump() @property @@ -276,11 +327,9 @@ class decort_vins(DecortController): ), ext_net_id=dict( type='int', - default=-1, ), ext_ip_addr=dict( type='str', - default='', ), ipcidr=dict( type='str', @@ -304,7 +353,6 @@ class decort_vins(DecortController): ), state=dict( type='str', - default='present', choices=[ 'absent', 'disabled', @@ -335,6 +383,9 @@ class decort_vins(DecortController): zone_id=dict( type=int, ), + security_group_mode=dict( + type='bool', + ), ), supports_check_mode=True, required_one_of=[ @@ -347,6 +398,34 @@ class decort_vins(DecortController): if self.check_aparam_zone_id() is False: check_errors = True + if ( + self.amodule.params['ext_ip_addr'] + and self.amodule.params['ext_net_id'] is None + and self.vins_info.vnfs.gw is None + ): + self.message( + msg=( + 'Check for parameter "ext_net_id" failed: ' + 'the "ext_net_id" parameter must be specified ' + 'if the "ext_ip_addr" parameter is passed and ' + 'VINS is not connected to an external network.' + ) + ) + check_errors = True + + if ( + self.aparams['security_group_mode'] is not None + and self.vins_info.security_group_mode != self.aparams['security_group_mode'] + ): + self.message( + msg=( + 'Check for parameter "security_group_mode" failed: ' + '"security_group_mode" cannot be changed ' + 'for existing ViNS' + ) + ) + check_errors = True + if check_errors: self.exit(fail=True) @@ -384,36 +463,54 @@ class decort_vins(DecortController): # if cconfig_save is true, only config save without other updates vins_should_exist = False - if self.vins_id: + if self._vins_info: vins_should_exist = True - if self.vins_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING"]: + if self._vins_info.status in [ + sdk_types.VINSStatus.MODELED, + sdk_types.VINSStatus.DISABLING, + sdk_types.VINSStatus.ENABLING, + sdk_types.VINSStatus.DELETING, + sdk_types.VINSStatus.DESTROYING, + ]: # error: nothing can be done to existing ViNS in the listed statii regardless of # the requested state self.result['failed'] = True self.result['changed'] = False - self.result['msg'] = ("No change can be done for existing ViNS ID {} because of its current " - "status '{}'").format(self.vins_id, self.vins_facts['status']) - elif self.vins_facts['status'] == "DISABLED": + self.result['msg'] = ( + f'No change can be done for existing ' + f'ViNS ID {self.vins_id} because of its ' + f'current status {self._vins_info.status}' + ) + elif self._vins_info.status == sdk_types.VINSStatus.DISABLED: if amodule.params['state'] == 'absent': self.delete() vins_should_exist = False - elif amodule.params['state'] in ('present', 'disabled'): + elif ( + amodule.params['state'] is None + or amodule.params['state'] in ('present', 'disabled') + ): # update ViNS, leave in disabled state self.action() elif amodule.params['state'] == 'enabled': # update ViNS and enable self.action('enabled') - elif self.vins_facts['status'] in ["CREATED", "ENABLED"]: + elif self._vins_info.status in [ + sdk_types.VINSStatus.CREATED, + sdk_types.VINSStatus.ENABLED, + ]: if amodule.params['state'] == 'absent': self.delete() vins_should_exist = False - elif amodule.params['state'] in ('present', 'enabled'): + elif ( + amodule.params['state'] is None + or amodule.params['state'] in ('present', 'enabled') + ): # update ViNS self.action() elif amodule.params['state'] == 'disabled': # disable and update ViNS self.action('disabled') - elif self.vins_facts['status'] == "DELETED": + elif self._vins_info.status == sdk_types.VINSStatus.DELETED: if amodule.params['state'] in ['present', 'enabled']: # restore and enable self.action(restore=True) @@ -426,28 +523,48 @@ class decort_vins(DecortController): elif amodule.params['state'] == 'disabled': self.error() vins_should_exist = False - elif self.vins_facts['status'] == "DESTROYED": - if amodule.params['state'] in ('present', 'enabled'): + elif self._vins_info.status == sdk_types.VINSStatus.DESTROYED: + state = amodule.params['state'] + if state is None: + state = 'present' + self.message( + msg=( + f'State not specified, ' + f'default value "{state}" will be used.' + ), + warning=True, + ) + if state in ('present', 'enabled'): # need to re-provision ViNS; self.create() vins_should_exist = True - elif amodule.params['state'] == 'absent': + elif state == 'absent': self.nop() vins_should_exist = False - elif amodule.params['state'] == 'disabled': + elif state == 'disabled': self.error() else: + state = amodule.params['state'] + if state is None: + state = 'present' + self.message( + msg=( + f'State not specified, ' + f'default value "{state}" will be used.' + ), + warning=True, + ) # Preexisting ViNS was not found. vins_should_exist = False # we will change it back to True if ViNS is created or restored # If requested state is 'absent' - nothing to do - if amodule.params['state'] == 'absent': + if state == 'absent': self.nop() - elif amodule.params['state'] in ('present', 'enabled'): + elif state in ('present', 'enabled'): self.check_amodule_argument('vins_name') # as we already have account ID and RG ID we can create ViNS and get vins_id on success self.create() vins_should_exist = True - elif amodule.params['state'] == 'disabled': + elif state == 'disabled': self.error() # # conditional switch end - complete module run @@ -457,7 +574,7 @@ class decort_vins(DecortController): else: # prepare ViNS facts to be returned as part of self.result and then call exit_json(...) if self.result['changed']: - _, self.vins_facts = self.vins_find(self.vins_id) + self._vins_info = self._vins_get_by_id(vins_id=self.vins_id) self.result['facts'] = self.package_facts(amodule.check_mode) amodule.exit_json(**self.result) diff --git a/library/decort_vm.py b/library/decort_vm.py index 90d546b..5d121a9 100644 --- a/library/decort_vm.py +++ b/library/decort_vm.py @@ -42,7 +42,7 @@ class decort_vm(DecortController): validated_acc_id = 0 validated_rg_id = 0 - validated_rg_facts = None + validated_rg_model = None self.vm_to_clone_id = 0 self.vm_to_clone_info = None @@ -118,21 +118,21 @@ class decort_vm(DecortController): self.fail_json(**self.result) # fail the module -> exit # now validate RG - validated_rg_id, validated_rg_facts = self.rg_find(validated_acc_id, + validated_rg_id, validated_rg_model = self.rg_find(validated_acc_id, arg_amodule.params['rg_id'], arg_amodule.params['rg_name']) - if not validated_rg_id: + if not validated_rg_id or not validated_rg_model: self.result['failed'] = True self.result['changed'] = False self.result['msg'] = "Cannot find RG ID {} / name '{}'.".format(arg_amodule.params['rg_id'], arg_amodule.params['rg_name']) - self.fail_json(**self.result) + self.amodule.fail_json(**self.result) # fail the module - exit self.rg_id = validated_rg_id arg_amodule.params['rg_id'] = validated_rg_id - arg_amodule.params['rg_name'] = validated_rg_facts['name'] - self.acc_id = validated_rg_facts['accountId'] + arg_amodule.params['rg_name'] = validated_rg_model.name + self.acc_id = validated_rg_model.account_id # at this point we are ready to locate Compute, and if anything fails now, then it must be # because this Compute does not exist or something goes wrong in the upstream API @@ -635,7 +635,11 @@ class decort_vm(DecortController): """Compute destroy handler for VM management by decort_vm module. Note that this handler deletes the VM permanently together with all assigned disk resources. """ - self.compute_delete(comp_id=self.comp_id, permanently=True) + self.sdk_checkmode(self.api.ca.compute.delete)( + vm_id=self.comp_id, + detach_disks=True, + permanently=True, + ) self.comp_id, self.comp_info, _ = self._compute_get_by_id(self.comp_id) return @@ -693,7 +697,13 @@ class decort_vm(DecortController): aparam_disk_id = aparam_boot['disk_id'] if aparam_disk_id is not None: for disk in self.comp_info['disks']: - if disk['id'] == aparam_disk_id and disk['type'] != 'B': + if ( + disk['id'] == aparam_disk_id + and not self.is_vm_boot_disk( + vm_chipset=self.comp_info['chipset'], + vm_disk=disk, + ) + ): self.compute_boot_disk( comp_id=self.comp_info['id'], boot_disk=aparam_disk_id, @@ -1000,7 +1010,10 @@ class decort_vm(DecortController): ret_dict['disks'] = self.comp_info['disks'] for disk in ret_dict['disks']: - if disk['type'] == 'B': + if self.is_vm_boot_disk( + vm_chipset=self.comp_info['chipset'], + vm_disk=disk, + ): # if it is a boot disk - store its size ret_dict['disk_size'] = disk['sizeMax'] @@ -1073,6 +1086,8 @@ class decort_vm(DecortController): ret_dict['read_only'] = self.comp_info['read_only'] + ret_dict['weight'] = self.comp_info['weight'] + return ret_dict def check_amodule_args_for_create(self): @@ -1199,7 +1214,7 @@ class decort_vm(DecortController): ) elif ( aparam_storage_policy_id - not in self.rg_info['storage_policy_ids'] + not in self.rg_info.storage_policy_ids ): check_errors = True self.message( @@ -1681,7 +1696,9 @@ class decort_vm(DecortController): if new_boot_disk_size is not None: boot_disk_size = 0 for disk in self.comp_info['disks']: - if disk['type'] == 'B': + if self.is_vm_boot_disk( + vm_chipset=self.comp_info['chipset'], vm_disk=disk, + ): boot_disk_size = disk['sizeMax'] break else: @@ -1844,7 +1861,9 @@ class decort_vm(DecortController): aparam_disks_ids = [disk['id'] for disk in aparam_disks] comp_boot_disk_id = None for comp_disk in self.comp_info['disks']: - if comp_disk['type'] == 'B': + if self.is_vm_boot_disk( + vm_chipset=self.comp_info['chipset'], vm_disk=comp_disk, + ): comp_boot_disk_id = comp_disk['id'] break disks_to_detach = [] @@ -2137,8 +2156,8 @@ class decort_vm(DecortController): vm_has_shared_sep_disk = False vm_disk_ids = [disk['id'] for disk in self.comp_info['disks']] for disk_id in vm_disk_ids: - _, disk_info = self._disk_get_by_id(disk_id=disk_id) - if disk_info['sepType'] == 'SHARED': + disk_info = self._disk_get_by_id(disk_id=disk_id) + if disk_info.sep_type == sdk_types.SEPType.SHARED: vm_has_shared_sep_disk = True break @@ -2230,7 +2249,9 @@ class decort_vm(DecortController): if disk_redeploy: vm_has_boot_disk = False for disk in self.comp_info['disks']: - if disk['type'] == 'B': + if self.is_vm_boot_disk( + vm_chipset=self.comp_info['chipset'], vm_disk=disk, + ): vm_has_boot_disk = True break if not vm_has_boot_disk: @@ -2341,7 +2362,9 @@ class decort_vm(DecortController): check_errors = False # check if account has vm feature “trunk” - if not self.check_account_vm_features(vm_feature=self.VMFeature.trunk): + if not self.check_account_vm_features( + vm_feature=sdk_types.VMFeature.TRUNK, + ): check_errors = True self.message( 'Check for parameter "networks" failed: ' @@ -2349,7 +2372,7 @@ class decort_vm(DecortController): 'trunk type networks ' ) # check if rg has vm feature “trunk” - if not self.check_rg_vm_features(vm_feature=self.VMFeature.trunk): + if not self.check_rg_vm_features(vm_feature=sdk_types.VMFeature.TRUNK): check_errors = True self.message( 'Check for parameter "networks" failed: ' diff --git a/library/decort_zone.py b/library/decort_zone.py index 16e7a87..91aba0b 100644 --- a/library/decort_zone.py +++ b/library/decort_zone.py @@ -40,7 +40,7 @@ class DecortZone(DecortController): zone_model = self.api.cloudapi.zone.get(id=self.id) except sdk_exceptions.RequestException as e: if ( - e.orig_exception.response + e.orig_exception.response is not None and e.orig_exception.response.status_code == 404 ): self.message( diff --git a/module_utils/decort_utils.py b/module_utils/decort_utils.py index 1e73451..53c97d1 100644 --- a/module_utils/decort_utils.py +++ b/module_utils/decort_utils.py @@ -37,14 +37,20 @@ class DecortController(object): """ acc_id: None | int = None - _acc_info: None | dict = None + _acc_info: None | sdk_types.CloudapiAccountGetResultModel = None rg_id: None | int = None - _rg_info: None | dict = None + _rg_info: None | sdk_types.CloudapiRgGetResultModel = None + lb_id: None | int = None + _lb_info: None | sdk_types.CloudapiLbGetResultModel = None + vins_id: None | int = None + _vins_info: None | sdk_types.CloudapiVinsGetResultModel = None + k8s_id: None | int = None + _k8s_info: None | dict = None _api: sdk_types.API | None = None _usermanager_whoami_result: None | dict = None - ANSIBLE_MODULES_VERSION = '11.0.3' - COMPATIBLE_SDK_MINOR_VERSION = '1.4' + ANSIBLE_MODULES_VERSION = '12.0.0' + COMPATIBLE_SDK_MINOR_VERSION = '1.5' VM_RESIZE_NOT = 0 VM_RESIZE_DOWN = 1 @@ -104,7 +110,7 @@ class DecortController(object): ) @staticmethod - def obj_not_found(obj: str, id: None | int = None) -> str: + def obj_not_found(obj: str, id: None | int | str = None) -> str: with_id = f' with ID={id}' if id else '' return ( f'Current user does not have access to the requested {obj}' @@ -112,7 +118,12 @@ class DecortController(object): ) @staticmethod - def obj_deleted(obj: str, id: int, permanently=False, already=False): + def obj_deleted( + obj: str, + id: None | int | str, + permanently: bool = False, + already: bool = False, + ) -> str: how_deleted = ' permanently' if permanently else ' to recycle bin' already_text = ' already' if already else '' return ( @@ -351,30 +362,54 @@ class DecortController(object): self.exit(fail=True) @property - def acc_info(self) -> dict: + def acc_info(self) -> sdk_types.CloudapiAccountGetResultModel: if self._acc_info is None: if not isinstance(self.acc_id, int): raise TypeError _, acc_info = self.account_find(account_id=self.acc_id) - if not isinstance(acc_info, dict): + if acc_info is None: raise TypeError self._acc_info = acc_info return self._acc_info @property def acc_zone_ids(self) -> list[int]: - return [zone['id'] for zone in self.acc_info['zoneIds']] + return [zone.id for zone in self.acc_info.zones] @property - def rg_info(self) -> dict: + def rg_info(self) -> sdk_types.CloudapiRgGetResultModel: if self._rg_info is None: if not isinstance(self.rg_id, int): raise TypeError _, rg_info = self.rg_find(arg_rg_id=self.rg_id) - if not isinstance(rg_info, dict): + if not isinstance(rg_info, sdk_types.CloudapiRgGetResultModel): raise TypeError self._rg_info = rg_info return self._rg_info + + @property + def lb_info(self) -> sdk_types.CloudapiLbGetResultModel: + if self._lb_info is None: + if not isinstance(self.lb_id, int): + raise TypeError + self._lb_info = self._lb_get_by_id(lb_id=self.lb_id) + return self._lb_info + + @property + def vins_info(self) -> sdk_types.CloudapiVinsGetResultModel: + if self._vins_info is None: + if not isinstance(self.vins_id, int): + raise TypeError + self._vins_info = self._vins_get_by_id(vins_id=self.vins_id) + return self._vins_info + + @property + def k8s_info(self) -> dict: + if self._k8s_info is None: + if not isinstance(self.k8s_id, int): + raise TypeError + self._k8s_info = self.k8s_get_by_id(k8s_id=self.k8s_id) + return self._k8s_info @property def usermanager_whoami_result(self) -> dict: @@ -788,12 +823,13 @@ class DecortController(object): def decort_api_call( self, - arg_req_function, + arg_req_function: Callable[..., requests.Response], arg_api_name, - arg_params, + arg_params=None, arg_files=None, not_fail_codes: None | list = None, accept_json_response: bool = False, + arg_json_body=None, ) -> requests.Response: """ Wrapper around DECORT API calls. It uses authorization mode and @@ -810,7 +846,10 @@ class DecortController(object): @param arg_api_name: a string containing the path to the API name under DECORT controller URL - @param arg_params: a dictionary containing parameters to be + @param arg_params: a dictionary containing query parameters to be + passed to the API call + + @param arg_json_body: a json serializable Python object to be passed to the API call @param arg_req_function: function object to be called as @@ -824,7 +863,7 @@ class DecortController(object): retry_counter = max_retries http_headers = dict() - api_resp = None + api_resp: None | requests.Response = None req_url = self.controller_url + arg_api_name @@ -835,11 +874,13 @@ class DecortController(object): while retry_counter > 0: try: api_resp = arg_req_function( - req_url, + req_url, files=arg_files, - params=arg_params, - headers=http_headers, - verify=self.verify_ssl) + params=arg_params, + json=arg_json_body, + headers=http_headers, + verify=self.verify_ssl, + ) except requests.exceptions.SSLError: self.message(self.MESSAGES.ssl_error(url=req_url)) self.exit(fail=True) @@ -868,7 +909,9 @@ class DecortController(object): f'Error when calling DECORT API {api_resp.url}' f', HTTP status code {api_resp.status_code}' f', reason "{api_resp.reason}"' - f', parameters {arg_params}, text {api_resp.text}.' + f', parameters {arg_params}' + f', request JSON body {arg_json_body}' + f', text {api_resp.text}.' ) self.amodule.fail_json(**self.result) return None # actually, this directive will never be executed as fail_json aborts the script @@ -879,59 +922,6 @@ class DecortController(object): format(api_resp.url, api_resp.status_code, api_resp.reason) self.amodule.fail_json(**self.result) return None - - @waypoint - def user_get(self, id: str) -> dict: - """ - Implementation of functionality of the API method - `/cloudapi/user/get`. - """ - - api_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/user/get', - arg_params={ - 'username': id, - }, - ) - - return api_resp.json() - - @waypoint - def user_resource_consumption(self) -> dict[str, dict]: - """ - Implementation of the functionality of API method - `/cloudapi/user/getResourceConsumption`. - """ - - api_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/user/getResourceConsumption', - arg_params={}, - ) - api_resp_json = api_resp.json() - - return { - 'resource_consumed': api_resp_json['Consumed'], - 'resource_reserved': api_resp_json['Reserved'], - } - - @waypoint - def user_api_methods(self, id: str) -> dict: - """ - Implementation of the functionality of API method - `/cloudapi/user/apiList`. - """ - - api_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/user/apiList', - arg_params={ - 'userId': id - }, - ) - - return api_resp.json() @waypoint def user_objects_search(self, search_string: str) -> list[dict]: @@ -1069,7 +1059,9 @@ class DecortController(object): bdisk_size = 0 bdisk_id = 0 for disk in comp_dict['disks']: - if disk['type'] == 'B': + if self.is_vm_boot_disk( + vm_chipset=comp_dict['chipset'], vm_disk=disk, + ): bdisk_size = disk['sizeMax'] bdisk_id = disk['id'] break @@ -1094,7 +1086,9 @@ class DecortController(object): self.result['failed'] = False self.result['changed'] = True for disk in comp_dict['disks']: - if disk['type'] == 'B': + if self.is_vm_boot_disk( + vm_chipset=comp_dict['chipset'], vm_disk=disk, + ): disk['sizeMax'] = new_size break return @@ -1185,7 +1179,6 @@ class DecortController(object): api_params = { 'computeId': comp_dict['id'], 'diskId': disk['id'], - 'diskType': 'D', 'pci_slot': pci_slot_num, 'bus_number': bus_num, } @@ -1222,33 +1215,6 @@ class DecortController(object): ) self.set_changed() - def compute_delete(self, comp_id, permanently=False,detach=True): - """Delete a Compute instance identified by its ID. It is assumed that the Compute with the specified - ID exists. - - @param (int) comp_id: ID of the Compute instance to be deleted - @param (bool) permanently: a bool that tells if deletion should be permanent. If False, the Compute - will be marked as deleted and placed into a "trash bin" for predefined period of time (usually, for a - few days). Until this period passes the Compute can be restored by calling 'restore' method. - """ - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "compute_delete") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = "compute_delete() in check mode: delete Compute ID {} was requested.".format(comp_id) - return - - api_params = dict(computeId=comp_id, - permanently=permanently, - detachDisks=detach, ) - self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/delete", api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['changed'] = True - - return comp_id - def _compute_get_by_id( self, comp_id, @@ -2765,12 +2731,12 @@ class DecortController(object): else: validated_acc_id = account_id if account_id == 0: - validated_rg_id, rg_facts = self._rg_get_by_id(rg_id) - if not validated_rg_id: + validated_rg_id, rg_model = self._rg_get_by_id(rg_id) + if not validated_rg_id or not rg_model: self.result['failed'] = True self.result['msg'] = ("Failed to find RG ID {}, and account ID is zero.").format(rg_id) return 0, None - validated_acc_id = rg_facts['accountId'] + validated_acc_id = rg_model.account_id api_params = dict(accountId=validated_acc_id) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/image/list", api_params) @@ -2832,12 +2798,12 @@ class DecortController(object): else: validated_acc_id = account_id if account_id == 0: - validated_rg_id, rg_facts = self._rg_get_by_id(rg_id) - if not validated_rg_id: + validated_rg_id, rg_model = self._rg_get_by_id(rg_id) + if not validated_rg_id or not rg_model: self.result['failed'] = True self.result['msg'] = ("Failed to find RG ID {}, and account ID is zero.").format(rg_id) return 0, None - validated_acc_id = rg_facts['accountId'] + validated_acc_id = rg_model.account_id api_params = dict(accountId=validated_acc_id) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/image/list", api_params) @@ -2884,18 +2850,6 @@ class DecortController(object): self.result['changed'] = True return 0, None - def image_delete(self, imageId): - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "image_delete") - - api_params = dict(imageId=imageId) - api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/image/delete", api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - image_dict = json.loads(api_resp.content.decode('utf8')) - - self.result['changed'] = True - return imageId - - def image_create( self, img_name, @@ -2952,15 +2906,6 @@ class DecortController(object): return 0, None - def image_rename(self, imageId, name): - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "image_rename") - api_params = dict(imageId=imageId, name=name,) - api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/image/rename", api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - link_image_dict = json.loads(api_resp.content.decode('utf8')) - self.result['failed'] = False - self.result['changed'] = True - @waypoint @checkmode def image_change_storage_policy( @@ -2987,33 +2932,9 @@ class DecortController(object): ################################### # Resource Group (RG) manipulation methods ################################### - def rg_delete(self, rg_id, permanently, recursively: bool): - """Deletes specified VDC. - - @param (int) rg_id: integer value that identifies the RG to be deleted. - @param (bool) permanently: a bool that tells if deletion should be permanent. If False, the RG will be - marked as deleted and placed into a trash bin for predefined period of time (usually, a few days). Until - this period passes the RG can be restored by calling the corresponding 'restore' method. - """ - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_delete") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = "rg_delete() in check mode: delete RG ID {} was requested.".format(rg_id) - return - - api_params = dict(rgId=rg_id, - force=recursively, - permanently=permanently, ) - self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/delete", api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['changed'] = True - - return - - def _rg_get_by_id(self, rg_id): + def _rg_get_by_id(self, rg_id) -> tuple[ + int, sdk_types.CloudapiRgGetResultModel | None + ]: """Helper function that locates RG by ID and returns RG facts. @param (int) )rg_id: ID of the RG to find and return facts for. @@ -3024,72 +2945,40 @@ class DecortController(object): suggested to check the return values accordingly. """ ret_rg_id = 0 - ret_rg_dict = dict() + rg_model: sdk_types.CloudapiRgGetResultModel | None = None if not rg_id: self.result['failed'] = True self.result['msg'] = "rg_get_by_id(): zero RG ID specified." self.amodule.fail_json(**self.result) - api_params = {'rgId': rg_id} - - # Get RG base info - api_rg_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/rg/get', - arg_params=api_params - ) - if api_rg_resp.status_code != 200: + try: + rg_model = self.api.cloudapi.rg.get(rg_id=rg_id) + except sdk_exceptions.RequestException as e: self.result['warning'] = ( f'rg_get_by_id(): failed to get RG by ID {rg_id}.' - f' HTTP code {api_rg_resp.status_code}' - f', response {api_rg_resp.reason}.' - ) - return ret_rg_id, ret_rg_dict - ret_rg_id = rg_id - ret_rg_dict = api_rg_resp.json() - - # Get RG resources info - rg_status = ret_rg_dict.get('status') - if not rg_status or rg_status in ('DELETED', 'DESTROYED'): - return ret_rg_id, ret_rg_dict - api_rg_res_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/rg/getResourceConsumption', - arg_params=api_params + f' HTTP code { + e.orig_exception.response.status_code + if e.orig_exception.response is not None else None + }' + f', response { + e.orig_exception.response.reason + if e.orig_exception.response is not None else None + }.' ) - if api_rg_res_resp.status_code != 200: - self.result['warning'] = ( - f'rg_get_by_id(): failed to get RG Resources by ID {rg_id}.' - f' HTTP code {api_rg_res_resp.status_code}' - f', response {api_rg_res_resp.reason}.' - ) - else: - ret_rg_dict['Resources'] = api_rg_res_resp.json() + return ret_rg_id, rg_model - return ret_rg_id, ret_rg_dict + ret_rg_id = rg_id - def rg_access(self, arg_rg_id, arg_access): - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_access") + return ret_rg_id, rg_model - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = ("rg_access() in check mode: access to RG id '{}' was " - "requested with '{}'.").format(arg_rg_id, arg_access) - return 0 - - api_params=dict(rgId=arg_rg_id,) - if arg_access['action'] == "grant": - api_params['user']=arg_access['user'], - api_params['right']=arg_access['right'], - api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/accessGrant", api_params) - else: - api_params['user']=arg_access['user'], - api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/accessRevoke", api_params) - self.result['changed'] = True - return - - def rg_find(self, arg_account_id=0, arg_rg_id=0, arg_rg_name="", arg_check_state=True): + def rg_find( + self, + arg_account_id=0, + arg_rg_id=0, + arg_rg_name="", + arg_check_state=True + ) -> tuple[int, sdk_types.CloudapiRgGetResultModel | None]: """Returns non zero RG ID and a dictionary with RG details on success, 0 and empty dictionary otherwise. This method does not fail the run if RG cannot be located by its name (arg_rg_name), because this could be an indicator of the requested RG never existed before. @@ -3114,28 +3003,28 @@ class DecortController(object): # Transient state (ending with ING) are invalid from RG manipulation viewpoint # - RG_INVALID_STATES = ["MODELED"] + RG_INVALID_STATES = [sdk_types.ResourceGroupStatus.MODELED] self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_find") ret_rg_id = 0 - api_params = dict() - ret_rg_dict = None + rg_model: sdk_types.CloudapiRgGetResultModel | None = None if arg_rg_id is not None and arg_rg_id > 0: - api_params['includedeleted'] = True - api_resp = self.decort_api_call( - requests.post, - '/restmachine/cloudapi/rg/list', - api_params, + rg_list = ( + self.api.cloudapi.rg.list( + include_deleted=True, + ).data ) - rg_list = json.loads(api_resp.content.decode('utf8')) - for rg_item in rg_list['data']: - if rg_item['id'] == arg_rg_id: - got_id, got_specs = self._rg_get_by_id(rg_item['id']) - if not arg_check_state or got_specs['status'] not in RG_INVALID_STATES: + for rg_item in rg_list: + if rg_item.id == arg_rg_id: + got_id, got_specs = self._rg_get_by_id(rg_item.id) + if got_specs and ( + not arg_check_state + or got_specs.status not in RG_INVALID_STATES + ): ret_rg_id = got_id - ret_rg_dict = got_specs + rg_model = got_specs break if not ret_rg_id: @@ -3155,22 +3044,24 @@ class DecortController(object): self.result['msg'] = "rg_find(): cannot find RG by name if account ID is zero or less." self.amodule.fail_json(**self.result) # try to locate RG by name - start with getting all RGs IDs within the specified account - api_params['accountId'] = arg_account_id - api_params['includedeleted'] = True #api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/account/listRG", api_params) - api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/list",api_params) - if api_resp.status_code == 200: - account_specs = json.loads(api_resp.content.decode('utf8')) - #api_params.pop('accountId') - for rg_item in account_specs['data']: - # - if rg_item['name'] == arg_rg_name: - # name matches - got_id, got_specs = self._rg_get_by_id(rg_item['id']) - if not arg_check_state or got_specs['status'] not in RG_INVALID_STATES: - ret_rg_id = got_id - ret_rg_dict = got_specs - break + rg_list = ( + self.api.cloudapi.rg.list( + include_deleted=True, + account_id=arg_account_id + ).data + ) + for rg_item in rg_list: + if rg_item.name == arg_rg_name: + # name matches + got_id, got_specs = self._rg_get_by_id(rg_item.id) + if got_specs and ( + not arg_check_state + or got_specs.status not in RG_INVALID_STATES + ): + ret_rg_id = got_id + rg_model = got_specs + break # Note: we do not fail the run if RG cannot be located by its name, because it could be a new RG # that never existed before. In this case ret_rg_id=0 and empty ret_rg_dict will be returned. else: @@ -3179,56 +3070,7 @@ class DecortController(object): self.result['msg'] = "rg_find(): either non-zero ID or a non-empty name must be specified." self.amodule.fail_json(**self.result) - return ret_rg_id, ret_rg_dict - - def rg_setDefNet(self, arg_rg_id, arg_net_type, arg_net_id): - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_setDefNet") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = ("rg_setDefNet() in check mode: setDefNet RG id '{}' was " - "requested.").format(arg_rg_id) - return 0 - - api_params = {'rgId': arg_rg_id} - - if arg_net_type == "NONE": - api_route = '/restmachine/cloudapi/rg/removeDefNet' - else: - api_route = '/restmachine/cloudapi/rg/setDefNet' - api_params.update({ - 'netType': arg_net_type, - 'netId': arg_net_id, - }) - - self.decort_api_call( - arg_req_function=requests.post, - arg_api_name=api_route, - arg_params=api_params, - ) - - self.result['changed'] = True - - return - - - def rg_enable(self, arg_rg_id, arg_state): - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_enable") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = ("rg_enable() in check mode: '{}' RG id '{}' was " - "requested.").format(arg_state, arg_rg_id) - return 0 - - api_params = dict(rgId=arg_rg_id) - - if arg_state == "enabled": - api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/enable", api_params) - else: - api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/disable", api_params) - self.result['changed'] = True - return + return ret_rg_id, rg_model def rg_provision( self, @@ -3320,7 +3162,15 @@ class DecortController(object): # TODO: this method will not work in its current implementation. Update it for new .../rg/update specs. - def rg_update(self, arg_rg_dict, arg_quotas, arg_res_types, arg_newname, arg_sep_pools, arg_desc: str | None = None): + def rg_update( + self, + rg_model: sdk_types.CloudapiRgGetResultModel, + arg_quotas, + arg_res_types: list, + arg_newname, + arg_sep_pools, + arg_desc: str | None = None, + ): """Manage quotas for an existing RG. @param arg_rg_dict: dictionary with RG facts as returned by rg_find(...) method or .../rg/get API @@ -3339,20 +3189,22 @@ class DecortController(object): if self.amodule.check_mode: self.result['failed'] = False self.result['msg'] = ("rg_update() in check mode: setting quotas on RG ID {}, RG name '{}' was " - "requested.").format(arg_rg_dict['id'], arg_rg_dict['name']) + "requested.").format(rg_model.id, rg_model.name) return update_required = False - api_params = dict(rgId=arg_rg_dict['id'],) + api_params: dict[str, Any] = { + 'rgId': rg_model.id, + } if arg_res_types: - if arg_rg_dict['resourceTypes'] != arg_res_types: + if rg_model.resource_types != arg_res_types: api_params['resourceTypes'] = arg_res_types update_required = True else: - api_params['resourceTypes'] = arg_rg_dict['resourceTypes'] + api_params['resourceTypes'] = rg_model.resource_types - if arg_newname != "" and arg_newname!=arg_rg_dict['name']: + if arg_newname != "" and arg_newname!=rg_model.name: api_params['name'] = arg_newname update_required = True @@ -3360,11 +3212,11 @@ class DecortController(object): # - when setting resource limits, the keys are in the form 'max{{ RESOURCE_NAME }}Capacity' # - when quering resource limits, the keys are in the form of cloud units (CU_*) query_key_map = dict( - cpu='CU_C', - ram='CU_M', - disk='CU_DM', - ext_ips='CU_I', - storage_policies='storage_policy', + cpu='cpu_count', + ram='ram_size_mb', + disk='storage_size_gb', + ext_ips='ext_ip_count', + storage_policies='storage_policies', ) set_key_map = dict( cpu='maxCPUCapacity', @@ -3374,7 +3226,7 @@ class DecortController(object): storage_policies='storage_policies', ) - rg_resource_limits_dict = arg_rg_dict['resourceLimits'] + rg_quotas = rg_model.quotas for quota_type in ( 'cpu', 'ram', 'disk', 'ext_ips', 'storage_policies', ): @@ -3387,16 +3239,14 @@ class DecortController(object): for aparam_storage_policy in arg_quotas[ 'storage_policies' ]: - for rg_storage_policy in rg_resource_limits_dict[ - 'storage_policy' - ]: + for rg_storage_policy in rg_quotas.storage_policies: if ( aparam_storage_policy['id'] - == rg_storage_policy['id'] + == rg_storage_policy.id and aparam_storage_policy[ 'storage_size_gb' ] - != rg_storage_policy['limit'] + != rg_storage_policy.storage_size_gb ): update_required = True quotas.append({ @@ -3411,16 +3261,30 @@ class DecortController(object): json.dumps(quotas) ) else: - if arg_quotas[quota_type] != rg_resource_limits_dict[query_key_map[quota_type]]: + if arg_quotas[quota_type] != getattr(rg_quotas, query_key_map[quota_type]): api_params[set_key_map[quota_type]] = arg_quotas[quota_type] update_required = True - elif arg_quotas[quota_type] == rg_resource_limits_dict[query_key_map[quota_type]]: + elif arg_quotas[quota_type] == getattr(rg_quotas, query_key_map[quota_type]): api_params[set_key_map[quota_type]] = arg_quotas[quota_type] else: - api_params[set_key_map[quota_type]] = rg_resource_limits_dict[query_key_map[quota_type]] + quota = getattr(rg_quotas, query_key_map[quota_type]) + if quota_type == 'storage_policies': + sp_quotas = [] + for sp_quota in quota: + sp_quotas.append( + json.dumps( + { + 'id': sp_quota.id, + 'limit': sp_quota.storage_size_gb, + } + ) + ) + api_params[set_key_map[quota_type]] = sp_quotas + else: + api_params[set_key_map[quota_type]] = quota else: # if quotas dictionary is None, it means that no quotas should be set - reset the limits - api_params[set_key_map[quota_type]] = rg_resource_limits_dict[query_key_map[quota_type]] + api_params[set_key_map[quota_type]] = getattr(rg_quotas, query_key_map[quota_type]) if arg_sep_pools is not None: if arg_sep_pools: @@ -3430,14 +3294,14 @@ class DecortController(object): sep_pools.add( f'{sep["sep_id"]}_{pool_name}' ) - if set(arg_rg_dict['uniqPools']) != sep_pools: + if set(rg_model.sep_pools) != sep_pools: api_params['uniqPools'] = sep_pools update_required = True - elif arg_rg_dict['uniqPools']: + elif rg_model.sep_pools: api_params['clearUniqPools'] = True update_required = True - if arg_desc is not None and arg_desc != arg_rg_dict['desc']: + if arg_desc is not None and arg_desc != rg_model.description: api_params['desc'] = arg_desc update_required = True @@ -3449,36 +3313,13 @@ class DecortController(object): return - def rg_restore(self, arg_rg_id): - """Restores previously deleted RG identified by its ID. For restore to succeed - the RG must be in 'DELETED' state. - - @param arg_rg_id: ID of the RG to restore. - """ - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_restore") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = "rg_restore() in check mode: restore RG ID {} was requested.".format(arg_rg_id) - return - - api_params = dict(rgId=arg_rg_id) - self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/restore", api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['changed'] = True - return - @waypoint def account_find( self, account_name: str = '', account_id=0, - audits=False, fail_if_not_found=False, - resource_consumption=False, - ): + ) -> tuple[int, sdk_types.CloudapiAccountGetResultModel | None]: """ Find account specified by account ID or name and return a turple with account ID and account info dict. @@ -3487,24 +3328,12 @@ class DecortController(object): @param (string) account_name: name of the account to find. - @param (bool) audits: If `True` is specified, - then the method `self.account_audits` - will be called passing founded account ID and result of - the call will be added to - account info dict (key `audits`). - @param (bool) fail_if_not_found: If `True` is specified, then the method `self.amodule.fail_json(**self.result)` will be called if account is not found. - @param (bool) resource_consumption: If `True` is specified, - then the method `self.account_resource_consumption` - will be called passing founded account ID and result of - the call will be added to - account info dict (key `resource_consumption`). - - Returns non zero account ID and account info dict on success, - 0 and empty dict otherwise (if `fail_if_not_found=False`). + Returns non zero account ID and account info model on success, + 0 and None otherwise (if `fail_if_not_found=False`). """ if not account_id and not account_name: @@ -3516,43 +3345,39 @@ class DecortController(object): _account_id = account_id if account_name and not account_id: - api_params = { - 'name': account_name - } - api_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/account/list', - arg_params=api_params - ) - accounts_list = api_resp.json()['data'] + accounts_list = ( + self.api.cloudapi.account.list( + name=account_name + ).data + ) for account in accounts_list: - if account['name'] == account_name: - _account_id = account['id'] + if account.name == account_name: + _account_id = account.id break else: - api_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/account/listDeleted', - arg_params=api_params - ) - deleted_accounts_list = api_resp.json()['data'] + deleted_accounts_list = ( + self.api.cloudapi.account.list_deleted( + name=account_name + ).data + ) for account in deleted_accounts_list: - if account['name'] == account_name: - _account_id = account['id'] + if account.name == account_name: + _account_id = account.id break if _account_id: - api_params = { - 'accountId': _account_id - } - api_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/account/get', - arg_params=api_params, - not_fail_codes=[404] + try: + account_details = ( + self.api.cloudapi.account.get( + account_id=_account_id, + ) ) - if api_resp.status_code == 200: - account_details = api_resp.json() + except sdk_exceptions.RequestException as e: + if ( + e.orig_exception.response is not None + and e.orig_exception.response.status_code != 404 + ): + raise e if not account_details: if fail_if_not_found: @@ -3562,130 +3387,7 @@ class DecortController(object): self.exit(fail=True) return 0, None - account_details['computes_amount'] = account_details.pop('computes') - account_details['vinses_amount'] = account_details.pop('vinses') - account_details['description'] = account_details.pop('desc') - - account_details['createdTime_readable'] = self.sec_to_dt_str( - account_details['createdTime'] - ) - account_details['deactivationTime_readable'] = self.sec_to_dt_str( - account_details['deactivationTime'] - ) - account_details['deletedTime_readable'] = self.sec_to_dt_str( - account_details['deletedTime'] - ) - account_details['updatedTime_readable'] = self.sec_to_dt_str( - account_details['updatedTime'] - ) - account_details['resourceLimits']['storage_policies'] = ( - account_details['resourceLimits'].pop('storage_policy') - ) - account_storage_policies = ( - account_details['resourceLimits']['storage_policies'] - ) - for storage_policy in account_storage_policies: - storage_policy['storage_size_gb'] = storage_policy.pop('limit') - - if resource_consumption: - resource_consumption_dict = ( - self.api.cloudapi.account.get_resource_consumption( - account_id=account_details['id'], - ).model_dump() - ) - resource_consumption_dict.pop('id') - resource_consumption_dict.pop('quotas') - account_details['resource_consumption'] = resource_consumption_dict - - if audits: - account_details['audits'] = self.account_audits( - account_id=account_details['id'], - fail_if_not_found=True - ) - - return account_details['id'], account_details - - @waypoint - def account_resource_consumption(self, account_id: int, - fail_if_not_found=False) -> None | dict: - """ - Implementation of functionality of the API method - `/cloudapi/account/getResourceConsumption`. - - - @param (bool) fail_if_not_found: If `True` is specified, then - the method `self.amodule.fail_json(**self.result)` will be - called if account is not found. - """ - - api_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/account/getResourceConsumption', - arg_params={'accountId': account_id}, - not_fail_codes=[404] - ) - if api_resp.status_code == 200: - return api_resp.json() - else: - if fail_if_not_found: - self.result['msg'] = ("Current user does not have access to" - " the requested account or non-existent" - " account specified.") - self.amodule.fail_json(**self.result) - - @waypoint - def account_audits(self, account_id: int, - fail_if_not_found=False) -> None | list: - """ - Implementation of functionality of the API method - `/cloudapi/account/audits`. - - - @param (bool) fail_if_not_found: If `True` is specified, then - the method `self.amodule.fail_json(**self.result)` will be - called if account is not found. - """ - - api_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/audit/list', - arg_params={'account_id': account_id}, - not_fail_codes=[404] - ) - - if api_resp.status_code != 200: - if fail_if_not_found: - self.result['msg'] = ("Current user does not have access to" - " the requested account or non-existent" - " account specified.") - self.amodule.fail_json(**self.result) - return - - audits = api_resp.json()['data'] - - for a in audits: - a['api_url_path'] = a.pop('call') - if 'apitask' in a: - a['async_request_task_id'] = a.pop('apitask') - if 'service_id' in a: - a['bservice_id'] = a.pop('service_id') - if 'flipgroup_id' in a: - a['flip_group_id'] = a.pop('flipgroup_id') - if 'resgroup_id' in a: - a['rg_id'] = a.pop('resgroup_id') - if 'compute_id' in a: - a['vm_id'] = a.pop('compute_id') - if 'timestampEnd' in a: - a['response_timestamp'] = a.pop('timestampEnd') - a['response_timestamp_readable'] = self.sec_to_dt_str(a['response_timestamp']) - a['request_timestamp'] = a.pop('timestamp') - a['user_name'] = a.pop('user') - a['client_ip_addr'] = a.pop('remote_addr') - a['request_datetime_iso8601'] = a.pop('_ttl') - a['status_code'] = a.pop('statuscode') - a['execution_time_sec'] = a.pop('responsetime') - - return audits + return account_details.id, account_details @waypoint @checkmode @@ -3853,78 +3555,6 @@ class DecortController(object): break time.sleep(sleep_interval) - @waypoint - @checkmode - def account_disable(self, account_id: int) -> None: - """ - Implementation of functionality of the API method - `/cloudapi/account/disable`. - - The method `self.exit(fail=True)` will be - called if account is not found. - """ - - OBJ = 'account' - - api_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/account/disable', - arg_params={ - 'accountId': account_id, - }, - not_fail_codes=[404] - ) - - if api_resp.status_code == 404: - self.message( - self.MESSAGES.obj_not_found(obj=OBJ, id=account_id) - ) - self.exit(fail=True) - - self.message( - self.MESSAGES.obj_disabled( - obj=OBJ, - id=account_id, - ) - ) - self.set_changed() - - @waypoint - @checkmode - def account_enable(self, account_id: int) -> None: - """ - Implementation of functionality of the API method - `/cloudapi/account/enable`. - - The method `self.exit(fail=True)` will be - called if account is not found. - """ - - OBJ = 'account' - - api_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/account/enable', - arg_params={ - 'accountId': account_id, - }, - not_fail_codes=[404] - ) - - if api_resp.status_code == 404: - self.message( - self.MESSAGES.obj_not_found(obj=OBJ, id=account_id) - ) - self.exit(fail=True) - - self.message( - self.MESSAGES.obj_enabled( - obj=OBJ, - id=account_id, - ) - ) - self.set_changed() - @waypoint @checkmode def account_change_acl(self, account_id: int, @@ -4039,99 +3669,6 @@ class DecortController(object): self.set_changed() - @waypoint - @checkmode - def account_update(self, account_id: int, - access_emails: None | bool = None, - name: None | str = None, - cpu_quota: None | int = None, - disks_size_quota: None | int = None, - gpu_quota: None | int = None, - public_ip_quota: None | int = None, - ram_quota: None | int = None, - sep_pools: None | Iterable[str] = None, - description: None | str = None, - default_zone_id: None | int = None, - ) -> None: - """ - Implementation of functionality of the API method - `/cloudapi/account/update`. - - The method `self.exit(fail=True)` will be - called if account is not found. - """ - - OBJ = 'account' - - api_resp = self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/account/update', - arg_params={ - 'accountId': account_id, - 'gpu_units': gpu_quota, - 'maxCPUCapacity': cpu_quota, - 'maxMemoryCapacity': ram_quota, - 'maxNumPublicIP': public_ip_quota, - 'maxVDiskCapacity': disks_size_quota, - 'name': name, - 'sendAccessEmails': access_emails, - 'uniqPools': sep_pools, - 'desc': description, - 'defaultZoneId': default_zone_id, - }, - not_fail_codes=[404] - ) - - if api_resp.status_code == 404: - self.message( - self.MESSAGES.obj_not_found(obj=OBJ, id=account_id) - ) - self.exit(fail=True) - - if access_emails is not None: - smth = 'sending access emails' - if access_emails: - self.message( - self.MESSAGES.obj_smth_enabled(obj=OBJ, id=account_id, - smth=smth) - ) - else: - self.message( - self.MESSAGES.obj_smth_disabled(obj=OBJ, id=account_id, - smth=smth) - ) - if name is not None: - self.message( - self.MESSAGES.obj_renamed(obj=OBJ, id=account_id, - new_name=name) - ) - quotas = { - 'CPU quota': cpu_quota, - 'disks size quota': disks_size_quota, - 'GPU quota': gpu_quota, - 'public IP amount quota': public_ip_quota, - 'RAM quota': ram_quota, - } - for q_name, q_value in quotas.items(): - if q_value is not None: - self.message( - self.MESSAGES.obj_smth_changed(obj=OBJ, id=account_id, - smth=q_name, - new_value=q_value) - ) - - if default_zone_id is not None: - self.message( - self.MESSAGES.obj_smth_changed( - obj=OBJ, - id=account_id, - smth='default_zone_id', - new_value=default_zone_id, - ) - ) - - self.set_changed() - ################################### # Workflow callback stub methods - not fully implemented yet ################################### @@ -4201,67 +3738,44 @@ class DecortController(object): # ViNS management # ############################## - def vins_delete(self, vins_id, permanently=False): - """Deletes specified ViNS. - - @param (int) vins_id: integer value that identifies the ViNS to be deleted. - @param (bool) arg_permanently: a bool that tells if deletion should be permanent. If False, the ViNS will be - marked as DELETED and placed into a trash bin for predefined period of time (usually, a few days). Until - this period passes this ViNS can be restored by calling the corresponding 'restore' method. - """ - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_delete") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = "vins_delete() in check mode: delete ViNS ID {} was requested.".format(vins_id) - return - - # - # TODO: need decision if deleting a VINS with connected computes is allowed (aka force=True) - # and implement this decision accordingly. - # - - api_params = dict(vinsId=vins_id, - # force=True | False, - permanently=permanently, ) - self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/delete", api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['changed'] = True - return - - def _vins_get_by_id(self, vins_id): + @handle_sdk_exceptions + def _vins_get_by_id( + self, + vins_id, + ) -> sdk_types.CloudapiVinsGetResultModel: """Helper function that locates ViNS by ID and returns ViNS facts. This function expects that the ViNS exists (albeit in DELETED or DESTROYED state) and will return 0 ViNS ID if not found. @param (int) vins_id: ID of the ViNS to find and return facts for. - @return: ViNS ID and a dictionary of ViNS facts as provided by vins/get API call. + @return: Vins model. - Note that if it fails to find the ViNS with the specified ID, it may return 0 for ID - and empty dictionary for the facts. So it is suggested to check the return values - accordingly in the upstream code. """ - ret_vins_id = 0 - ret_vins_dict = dict() - if not vins_id: - self.result['failed'] = True - self.result['msg'] = "vins_get_by_id(): zero ViNS ID specified." - self.amodule.fail_json(**self.result) + self.message('vins_get_by_id(): zero ViNS ID specified.') + self.exit(fail=True) - api_params = dict(vinsId=vins_id, ) - api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/get", api_params) - if api_resp.status_code == 200: - ret_vins_id = vins_id - ret_vins_dict = json.loads(api_resp.content.decode('utf8')) - else: - self.result['warning'] = ("vins_get_by_id(): failed to get VINS by ID {}. HTTP code {}, " - "response {}.").format(vins_id, api_resp.status_code, api_resp.reason) + try: + vins_model = self.api.cloudapi.vins.get( + vins_id=vins_id + ) + return vins_model + 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='vins', + id=vins_id, + ) + ) + self.exit(fail=True) + else: + raise e - return ret_vins_id, ret_vins_dict def _rg_listvins(self,rg_id): """List all ViNS in the resource group @param (int) rg_id: id onr resource group @@ -4281,8 +3795,18 @@ class DecortController(object): return [] return ret_rg_vins_list['data'] - - def vins_find(self, vins_id, vins_name="", account_id=0, rg_id=0, rg_facts="", check_state=True): + + @handle_sdk_exceptions + def vins_find( + self, + vins_id, + vins_name="", + account_id=0, + rg_id=0, + rg_facts="", + check_state=True, + ): + """Find specified ViNS. @param (int) vins_id: ID of the ViNS. If non-zero vins_id is specified, all other arguments @@ -4302,28 +3826,34 @@ class DecortController(object): """ # transient and deleted/destroyed states are deemed invalid - VINS_INVALID_STATES = ["ENABLING", "DISABLING", "DELETING", "DESTROYING"] - + VINS_INVALID_STATES = [ + sdk_types.VINSStatus.ENABLING, + sdk_types.VINSStatus.DISABLING, + sdk_types.VINSStatus.DELETING, + sdk_types.VINSStatus.DESTROYING, + ] ret_vins_id = 0 - api_params: dict = { - 'includeDeleted': True, - } - ret_vins_facts = None self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_find") if vins_id > 0: - got_id, got_specs = self._vins_get_by_id(vins_id) - if got_specs['status'] not in VINS_INVALID_STATES: - ret_vins_id = got_id - ret_vins_facts = got_specs + try: + ret_vins_model = self.api.cloudapi.vins.get( + vins_id=vins_id, + ) + except sdk_exceptions.RequestException as e: + if ( + e.orig_exception.response is not None + and e.orig_exception.response.status_code == 404 + ): + return 0, None + else: + raise e - if not ret_vins_id: - self.result['failed'] = True - self.result['msg'] = "vins_find(): cannot find ViNS by ID {}.".format(vins_id) - self.amodule.fail_json(**self.result) - if not check_state or ret_vins_facts['status'] not in VINS_INVALID_STATES: - return ret_vins_id, ret_vins_facts + ret_vins_id = ret_vins_model.id + + if not check_state or ret_vins_model.status not in VINS_INVALID_STATES: + return ret_vins_id, ret_vins_model else: return 0, None elif vins_name != "": @@ -4338,11 +3868,19 @@ class DecortController(object): list_vins = self._rg_listvins(rg_id) for vins in list_vins: if vins['name'] == vins_name: - ret_vins_id, ret_vins_facts = self._vins_get_by_id(vins['id']) - if not check_state or ret_vins_facts['status'] not in VINS_INVALID_STATES: - return ret_vins_id, ret_vins_facts - else: - return 0, None + ret_vins_model = self.api.cloudapi.vins.get( + vins_id=vins['id'], + ) + ret_vins_id = ret_vins_model.id + if ( + not check_state + or ( + ret_vins_model.status not in VINS_INVALID_STATES + ) + ): + return ret_vins_id, ret_vins_model + + return 0, None elif account_id > 0: # search for ViNS at account level # validated_id, validated_facts = self.account_find("", account_id) @@ -4353,13 +3891,20 @@ class DecortController(object): # NOTE: account's 'vins' attribute does not list destroyed ViNSes! account_vinses = self._get_all_account_vinses(account_id) for vins in account_vinses: - ret_vins_id, ret_vins_facts = self._vins_get_by_id(vins['id']) - if ret_vins_id and ret_vins_facts['name'] == vins_name: - if not check_state or ret_vins_facts['status'] not in VINS_INVALID_STATES: - return ret_vins_id, ret_vins_facts - else: - return 0, None - else: # both Account ID and RG ID are zero - fail the module + if vins['name'] == vins_name: + ret_vins_model = self.api.cloudapi.vins.get( + vins_id=vins['id'], + ) + ret_vins_id = ret_vins_model.id + if ( + not check_state + or ret_vins_model.status not in VINS_INVALID_STATES + ): + return ret_vins_id, ret_vins_model + + return 0, None + else: + # both Account ID and RG ID are zero - fail the module self.result['failed'] = True self.result['msg'] = ("vins_find(): cannot find ViNS by name '{}' " "when no account ID or RG ID is specified.").format(vins_name) @@ -4373,14 +3918,15 @@ class DecortController(object): def vins_provision( self, - vins_name, - account_id, - rg_id=0, - ipcidr="", - ext_net_id=-1, - ext_ip_addr="", - desc="", - zone_id: None | int = None, + vins_name: str, + account_id: int, + security_group_mode: bool, + rg_id: int | None = None, + ipcidr: str | None = None, + ext_net_id: int = -1, + ext_ip_addr: str | None = None, + desc: str | None = None, + zone_id: int | None = None, ): """Provision ViNS according to the specified arguments. If critical error occurs the embedded call to API function will abort further execution of @@ -4407,143 +3953,122 @@ class DecortController(object): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_provision") - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = ("vins_provision() in check mode: provision ViNS name '{}' was " - "requested.").format(vins_name) - return 0 - if vins_name == "": self.result['failed'] = True self.result['msg'] = "vins_provision(): ViNS name cannot be empty." self.amodule.fail_json(**self.result) - api_url = "" - api_params = None if account_id and not rg_id: - api_url = "/restmachine/cloudapi/vins/createInAccount" target_gid = self.gid_get("") if not target_gid: self.result['failed'] = True self.result['msg'] = "vins_provision() failed to obtain Grid ID for default location." self.amodule.fail_json(**self.result) - api_params = dict( + ret_vins_id = self.sdk_checkmode( + self.api.ca.vins.create_in_account + )( name=vins_name, - accountId=account_id, - gid=target_gid, + account_id=account_id, + description=desc, + grid_id=target_gid, + ip_cidr=ipcidr, + zone_id=zone_id, + security_group_mode=security_group_mode, ) elif rg_id: - api_url = "/restmachine/cloudapi/vins/createInRG" - api_params = dict( + ret_vins_id = self.sdk_checkmode( + self.api.ca.vins.create_in_rg + )( name=vins_name, - rgId=rg_id, - extNetId=ext_net_id, + rg_id=rg_id, + description=desc, + ext_net_id=ext_net_id, + ext_net_ip=ext_ip_addr, + ip_cidr=ipcidr, + zone_id=zone_id, + security_group_mode=security_group_mode, ) - if ext_ip_addr: - api_params['extIp'] = ext_ip_addr else: self.result['failed'] = True self.result['msg'] = "vins_provision(): either account ID or RG ID must be specified." self.amodule.fail_json(**self.result) - if ipcidr != "": - api_params['ipcidr'] = ipcidr - - api_params['desc'] = desc - - api_params['zoneId'] = zone_id - - api_resp = self.decort_api_call(requests.post, api_url, api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - # API /restmachine/cloudapi/vins/create*** returns ID of the newly created ViNS on success - self.result['failed'] = False - self.result['changed'] = True - ret_vins_id = int(api_resp.content.decode('utf8')) return ret_vins_id - def vins_restore(self, vins_id): - """Restores previously deleted ViNS identified by its ID. For restore to succeed - the ViNS must be in 'DELETED' state. - - @param vins_id: ID of the ViNS to restore. - - @returns: nothing on success. On error this method will abort module execution. - """ - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_restore") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = "vins_restore() in check mode: restore ViNS ID {} was requested.".format(vins_id) - return - - api_params = dict(vinsId=vins_id) - self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/restore", api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['changed'] = True - return - - def vins_state(self, vins_dict, desired_state): + def vins_state( + self, + vins_model: sdk_types.CloudapiVinsGetResultModel, + desired_state: str, + ): """Enable or disable ViNS. - @param vins_dict: dictionary with the target ViNS facts as returned by vins_find(...) method or - .../vins/get API call. + @param vins_model: Vins model as returned by vins_find. @param desired_state: the desired state for this ViNS. Valid states are 'enabled' and 'disabled'. """ - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_state") - - NOP_STATES_FOR_VINS_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING", - "DESTROYED"] + NOP_STATES_FOR_VINS_CHANGE = [ + sdk_types.VINSStatus.MODELED, + sdk_types.VINSStatus.DISABLING, + sdk_types.VINSStatus.ENABLING, + sdk_types.VINSStatus.DELETING, + sdk_types.VINSStatus.DELETED, + sdk_types.VINSStatus.DESTROYING, + sdk_types.VINSStatus.DESTROYED, + ] VALID_TARGET_STATES = ["enabled", "disabled"] - if vins_dict['status'] in NOP_STATES_FOR_VINS_CHANGE: + if vins_model.status in NOP_STATES_FOR_VINS_CHANGE: self.result['failed'] = False - self.result['msg'] = ("vins_state(): no state change possible for ViNS ID {} " - "in its current state '{}'.").format(vins_dict['id'], vins_dict['status']) + self.result['msg'] = ( + f'vins_state(): no state change possible ' + f'for ViNS ID {vins_model.id} in its current' + f' state "{vins_model.status}".') return if desired_state not in VALID_TARGET_STATES: self.result['failed'] = False - self.result['warning'] = ("vins_state(): unrecognized desired state '{}' requested " - "for ViNS ID {}. No ViNS state change will be done.").format(desired_state, - vins_dict['id']) + self.result['warning'] = ( + f'vins_state(): unrecognized desired state ' + f'"{desired_state}" requested for ViNS ID ' + f'{vins_model.id}. No ViNS state change will be done.' + ) return if self.amodule.check_mode: self.result['failed'] = False - self.result['msg'] = ("vins_state() in check mode: setting state of ViNS ID {}, name '{}' to " - "'{}' was requested.").format(vins_dict['id'], vins_dict['name'], - desired_state) + self.result['msg'] = ( + f'vins_state() in check mode: setting ' + f'state of ViNS ID {vins_model.id}, ' + f'name "{vins_model.name}" to ' + f'"{desired_state}" was requested.' + ) return - vinsstate_api = "" # this string will also be used as a flag to indicate that API call is necessary - api_params = dict(vinsId=vins_dict['id']) - expected_state = "" + if ( + vins_model.status in [ + sdk_types.VINSStatus.CREATED, + sdk_types.VINSStatus.ENABLED, + ] + and desired_state == 'disabled' + ): + self.sdk_checkmode(self.api.cloudapi.vins.disable)( + vins_id=vins_model.id + ) + elif ( + vins_model.status == sdk_types.VINSStatus.DISABLED + and desired_state == 'enabled' + ): + self.sdk_checkmode(self.api.cloudapi.vins.enable)( + vins_id=vins_model.id + ) - if vins_dict['status'] in ["CREATED", "ENABLED"] and desired_state == 'disabled': - vinsstate_api = "/restmachine/cloudapi/vins/disable" - expected_state = "DISABLED" - elif vins_dict['status'] == "DISABLED" and desired_state == 'enabled': - vinsstate_api = "/restmachine/cloudapi/vins/enable" - expected_state = "ENABLED" - - if vinsstate_api != "": - self.decort_api_call(requests.post, vinsstate_api, api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['changed'] = True - vins_dict['status'] = expected_state - else: - self.result['failed'] = False - self.result['msg'] = ("vins_state(): no state change required for ViNS ID {} from current " - "state '{}' to desired state '{}'.").format(vins_dict['id'], - vins_dict['status'], - desired_state) return - def vins_update_extnet(self, vins_dict, ext_net_id, ext_ip_addr=""): + def vins_update_extnet( + self, + vins_model: sdk_types.CloudapiVinsGetResultModel, + ext_net_id: int | None, + ext_ip_addr: str | None, + ): """Update ViNS. Currently only updates to the external network connection settings and external IP address assignment are implemented. Note that as ViNS created at account level cannot have external connections, attempt @@ -4559,144 +4084,150 @@ class DecortController(object): recommended to update ViNS facts in the upstream code. """ - api_params = dict(vinsId=vins_dict['id'], ) - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_update") - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = ("vins_update_extnet() in check mode: updating ViNS ID {}, name '{}' " - "was requested.").format(vins_dict['id'], vins_dict['name']) - return - - if not vins_dict['rgId']: + if not vins_model.rg_id: # this ViNS exists at account level - no updates are possible - self.result['warning'] = ("vins_update(): no update is possible for ViNS ID {} " - "as it exists at account level.").format(vins_dict['id']) + self.result['warning'] = ( + f'vins_update(): no update is possible for ViNS ' + f'ID {vins_model.id} as it exists at account level.' + ) return gw_config = None - if vins_dict['vnfs'].get('GW'): - gw_config = vins_dict['vnfs']['GW']['config'] + if vins_model.vnfs.gw: + gw_config = vins_model.vnfs.gw.config - if ext_net_id < 0: + if ext_net_id and ext_net_id < 0: # Request to have ViNS disconnected from external network if gw_config: # ViNS is connected to external network indeed - call API to disconnect; otherwise - nothing to do - self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/extNetDisconnect", api_params) - self.result['failed'] = False - self.result['changed'] = True + self.sdk_checkmode(self.api.ca.vins.ext_net_disconnect)( + vins_id=vins_model.id, + ) # On success the above call will return here. On error it will abort execution by calling fail_json. - elif ext_net_id > 0: + elif ext_net_id and ext_net_id > 0: if gw_config: # Request to have ViNS connected to the specified external network # First check that if we are not connected to the same network already; otherwise - nothing to do - if gw_config['ext_net_id'] != ext_net_id: + if gw_config.ext_net_id != ext_net_id: # disconnect from current, we already have vinsId in the api_params - self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/extNetDisconnect", api_params) - self.result['changed'] = True + self.sdk_checkmode(self.api.ca.vins.ext_net_disconnect)( + vins_id=vins_model.id, + ) # connect to the new - api_params['netId'] = ext_net_id - api_params['ip'] = ext_ip_addr - self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/extNetConnect", api_params) - self.result['failed'] = False + self.sdk_checkmode(self.api.ca.vins.ext_net_connect)( + ext_net_id=ext_net_id, + ip_addr=ext_ip_addr, + vins_id=vins_model.id, + ) # On success the above call will return here. On error it will abort execution by calling fail_json. else: - self.result['failed'] = False - self.result['warning'] = ("vins_update(): ViNS ID {} is already connected to ext net ID {}, " - "ignore ext IP address change if any.").format(vins_dict['id'], - ext_net_id) + self.result['warning'] = ( + f'vins_update(): ViNS ID {vins_model.id} is already ' + f'connected to ext net ID {ext_net_id}, ignore ext ' + f'IP address change if any.' + ) elif gw_config is None: # connect to the new - api_params['netId'] = ext_net_id - api_params['ip'] = ext_ip_addr - self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/extNetConnect", api_params) - self.set_changed() - else: # ext_net_id = 0, i.e. connect ViNS to default network + self.sdk_checkmode(self.api.ca.vins.ext_net_connect)( + ext_net_id=ext_net_id, + ip_addr=ext_ip_addr, + vins_id=vins_model.id, + ) + elif ext_net_id == 0: # ext_net_id = 0, i.e. connect ViNS to default network # we will connect ViNS to default network only if it is NOT connected to any ext network yet if not gw_config: - api_params['netId'] = 0 - self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/extNetConnect", api_params) - self.result['changed'] = True - self.result['failed'] = False + self.sdk_checkmode(self.api.ca.vins.ext_net_connect)( + ext_net_id=0, + vins_id=vins_model.id, + ) else: - self.result['failed'] = False - self.result['warning'] = ("vins_update(): ViNS ID {} is already connected to ext net ID {}, " - "no reconnection to default network will be done.").format(vins_dict['id'], - gw_config[ - 'ext_net_id']) + self.result['warning'] = ( + f'vins_update(): ViNS ID {vins_model.id} is already ' + f'connected to ext net ID {gw_config.ext_net_id}, ' + f'no reconnection to default network will be done.' + ) + return - def vins_update_mgmt(self, vins_dict, mgmtaddr=[]): + + def vins_update_mgmt(self, vins_model: sdk_types.CloudapiVinsGetResultModel, mgmtaddr=[]): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_update_mgmt") if self.amodule.check_mode: self.result['failed'] = False self.result['msg'] = ("vins_update_mgmt() in check mode: updating ViNS ID {}, name '{}' " - "was requested.").format(vins_dict['id'], vins_dict['name']) + "was requested.").format(vins_model.id, vins_model.name) return - if self.amodule.params['config_save'] and vins_dict['VNFDev']['customPrecfg']: + if self.amodule.params['config_save'] and vins_model.vnfdev.custom_pre_cfg: # only save config,no other modifictaion self.result['changed'] = True - self._vins_vnf_config_save(vins_dict['VNFDev']['id']) + self._vins_vnf_config_save(vins_model.vnfdev.id) self.result['changed'] = True self.result['failed'] = False return - for iface in vins_dict['VNFDev']['interfaces']: - if iface['ipAddress'] in mgmtaddr and not iface['listenSsh']: - self._vins_vnf_addmgmtaddr(vins_dict['VNFDev']['id'],iface['ipAddress']) + for iface in vins_model.vnfdev.interfaces: + if iface.ip_addr in mgmtaddr and not iface.listen_ssh: + self._vins_vnf_addmgmtaddr(vins_model.vnfdev.id, iface.ip_addr) self.result['changed'] = True self.result['failed'] = False - elif iface['ipAddress'] not in mgmtaddr and iface['listenSsh']: - if iface['name'] != "ens9": - self._vins_vnf_delmgmtaddr(vins_dict['VNFDev']['id'],iface['ipAddress']) + elif iface.ip_addr not in mgmtaddr and iface.listen_ssh: + if iface.name != 'ens9': + self._vins_vnf_delmgmtaddr(vins_model.vnfdev.id, iface.ip_addr) self.result['changed'] = True self.result['failed'] = False if self.amodule.params['custom_config']: - if not vins_dict['VNFDev']['customPrecfg']: - self._vins_vnf_config_save(vins_dict['VNFDev']['id']) - self._vins_vnf_customconfig_set(vins_dict['VNFDev']['id']) + if not vins_model.vnfdev.custom_pre_cfg: + self._vins_vnf_config_save(vins_model.vnfdev.id) + self._vins_vnf_customconfig_set(vins_model.vnfdev.id) self.result['changed'] = True self.result['failed'] = False else: - if vins_dict['VNFDev']['customPrecfg']: - self._vins_vnf_customconfig_set(vins_dict['VNFDev']['id'],False) + if vins_model.vnfdev.custom_pre_cfg: + self._vins_vnf_customconfig_set(vins_model.vnfdev.id, False) self.result['changed'] = True self.result['failed'] = False return - def vins_update_ifaces(self,vins_dict,vinses=""): + def vins_update_ifaces(self, vins_model: sdk_types.CloudapiVinsGetResultModel, vinses=""): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_update_ifaces") - + if self.amodule.check_mode: self.result['failed'] = False self.result['msg'] = ("vins_update_iface() in check mode: updating ViNS ID {}, name '{}' " - "was requested.").format(vins_dict['id'], vins_dict['name']) + "was requested.").format(vins_model.id, vins_model.name) return list_ifaces_ip = [rec['ipaddr'] for rec in vinses] vinsid_not_existed = [] - for iface in vins_dict['VNFDev']['interfaces']: - if iface['connType'] == "VXLAN" and iface['type'] == "CUSTOM": - if iface['ipAddress'] not in list_ifaces_ip: - self._vnf_iface_remove(vins_dict['VNFDev']['id'],iface['name']) + for iface in vins_model.vnfdev.interfaces: + if iface.conn_type == 'VXLAN' and iface.type == 'CUSTOM': + if iface.ip_addr not in list_ifaces_ip: + self._vnf_iface_remove(vins_model.vnfdev.id, iface.name) self.result['changed'] = True self.result['failed'] = False else: #existed_conn_ip.append(iface['ipAddress']) - vinses = list(filter(lambda i: i['ipaddr']!=iface['ipAddress'],vinses)) + vinses = list(filter(lambda i: i['ipaddr']!=iface.ip_addr, vinses)) if not vinses: return - list_account_vins = self._get_all_account_vinses(vins_dict['VNFDev']['accountId']) + list_account_vins = self._get_all_account_vinses(vins_model.vnfdev.account_id) list_account_vinsid = [rec['id'] for rec in list_account_vins] for vins in vinses: if vins['id'] in list_account_vinsid: - _,v_dict = self._vins_get_by_id(vins['id']) + ret_vins_model = self._vins_get_by_id(vins_id=vins['id']) #TODO: vins reservation - self._vnf_iface_add(vins_dict['VNFDev']['id'],v_dict['vxlanId'],vins['ipaddr'],vins['netmask']) + if ret_vins_model: + self._vnf_iface_add( + arg_devid=vins_model.vnfdev.id, + arg_vxlanid=ret_vins_model.vxlan_id, + arg_ipaddr=vins['ipaddr'], + arg_netmask=vins['netmask'], + ) self.result['changed'] = True self.result['failed'] = False else: @@ -4704,7 +4235,7 @@ class DecortController(object): if vinsid_not_existed: self.result['warning'] = ("List ViNS id: {} that not created on account id: {}").format( vinsid_not_existed, - vins_dict['VNFDev']['accountId'] + vins_model.vnfdev.account_id ) return @@ -4864,48 +4395,50 @@ class DecortController(object): if val and val < MIN_IOPS: self.result['msg'] = (f"{arg} was set below the minimum iops {MIN_IOPS}: {val} provided") return - - def _disk_get_by_id(self, disk_id): + + @handle_sdk_exceptions + def _disk_get_by_id( + self, + disk_id, + )-> sdk_types.CloudapiDisksGetResultModel: """Helper function that locates Disk by ID and returns Disk facts. This function expects that the Disk exists (albeit in DELETED or DESTROYED or PURGED state) and will return zero ID if Disk is not found. @param (int) disk_id: ID of the disk to find and return facts for. - @return: Disk ID and a dictionary of disk facts as provided by disks/get API call. - - Note that if it fails to find the Disk with the specified ID, it may return 0 for ID - and empty dictionary for the facts. So it is suggested to check the return values - accordingly in the upstream code. + @return: Disk model. """ - ret_disk_id = 0 - ret_disk_dict = dict() - if not disk_id: - self.result['failed'] = True - self.result['msg'] = "disk_get_by_id(): zero Disk ID specified." - self.amodule.fail_json(**self.result) + try: + disk_model = self.api.cloudapi.disks.get( + disk_id=disk_id, + ) + return disk_model - api_params = dict(diskId=disk_id, ) - api_resp = self.decort_api_call( - requests.post, - '/restmachine/cloudapi/disks/get', - api_params, - not_fail_codes=[404], - ) - if api_resp.status_code == 200: - ret_disk_id = disk_id - ret_disk_dict = json.loads(api_resp.content.decode('utf8')) - elif api_resp.status_code == 404: - self.message(f'Disk with ID {disk_id} not found.') - self.exit(fail=True) - else: - self.result['warning'] = ("disk_get_by_id(): failed to get Disk by ID {}. HTTP code {}, " - "response {}.").format(disk_id, api_resp.status_code, api_resp.reason) + 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='disks', + id=disk_id, + ) + ) + self.exit(fail=True) + raise e - return ret_disk_id, ret_disk_dict - - def disk_find(self, disk_id=0, name="", account_id=0, check_state=False): + @handle_sdk_exceptions + def disk_find( + self, + disk_id=0, + name="", + account_id=0, + check_state=False, + fail_if_not_found=True, + ): """Find specified Disk. @param (int) disk_id: ID of the Disk. If non-zero disk_id is specified, all other arguments @@ -4927,41 +4460,60 @@ class DecortController(object): DISK_INVALID_STATES = ["MODELED", "CREATING", "DELETING", "DESTROYING"] ret_disk_id = 0 - ret_disk_facts = None + ret_disk_model = None if disk_id: - ret_disk_id, ret_disk_facts = self._disk_get_by_id(disk_id) + try: + ret_disk_model = self.api.cloudapi.disks.get( + disk_id=disk_id, + ) + except sdk_exceptions.RequestException as e: + if ( + e.orig_exception.response is not None + and e.orig_exception.response.status_code == 404 + ): + if fail_if_not_found: + self.message( + self.MESSAGES.obj_not_found( + obj='disk', + id=disk_id, + ) + ) + self.exit(fail=True) + else: + return 0, None + raise e + + ret_disk_id = ret_disk_model.id - if not ret_disk_id: - self.result['failed'] = True - self.result['msg'] = "disk_find(): cannot find Disk by ID {}.".format(disk_id) - self.amodule.fail_json(**self.result) - if not check_state or ret_disk_facts['status']: - return ret_disk_id, ret_disk_facts + if not check_state or ret_disk_model.status: + return ret_disk_id, ret_disk_model else: return 0, None elif name: if account_id: - api_params = { - 'accountId': account_id, - 'name': name, - 'show_all': True - } - api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/search", api_params) - disks_list = api_resp.json() - # Filtering disks by status - excluded_statuses = ('PURGED', 'DESTROYED') - filter_f = lambda x: x.get('status') not in excluded_statuses - disks_list = [d for d in disks_list if filter_f(d)] + disk_model_list = self.api.cloudapi.disks.list(account_id=account_id, name=name).data + excluded_statuses = (sdk_types.DiskStatus.PURGED, sdk_types.DiskStatus.DESTROYED) + filter_f = lambda x: x.status not in excluded_statuses + disks_list = [d for d in disk_model_list if filter_f(d)] # the above call may return more than one matching disk if len(disks_list) == 0: - return 0, None + if fail_if_not_found: + self.message( + self.MESSAGES.obj_not_found( + obj='disk', + id=disk_id, + ) + ) + self.exit(fail=True) + else: + return 0, None elif len(disks_list) > 1: self.result['failed'] = True self.result['msg'] = "disk_find(): Found more then one Disk with Name: {}.".format(name) self.amodule.fail_json(**self.result) else: - return disks_list[0]['id'], disks_list[0] + return disks_list[0].id, disks_list[0] else: # we are missing meaningful account_id - fail the module self.result['failed'] = True self.result['msg'] = ("disk_find(): cannot find Disk by name '{}' " @@ -4974,6 +4526,14 @@ class DecortController(object): return 0, None + def is_vm_boot_disk(self, vm_chipset: str, vm_disk: dict) -> bool: + if vm_chipset == 'Q35': + return vm_disk['bus_number'] == 6 + elif vm_chipset == 'i440fx': + return vm_disk['pci_slot'] == 6 + else: + self.message(msg=f'Unknown chipset: {vm_chipset}') + self.exit(fail=True) ############################## # @@ -4999,15 +4559,19 @@ class DecortController(object): return filtered_rules - def pfw_configure(self, comp_facts, vins_facts, new_rules=None): + def pfw_configure( + self, + comp_facts, + vins_model: sdk_types.CloudapiVinsGetResultModel, + new_rules: list[dict] | None = None, + ): """Manage port forwarding rules for Compute in a smart way. The method will try to match existing rules against the new rules set and calculate the delta settings to apply to the corresponding virtual network function. @param (dict) comp_facts: dictionary with Compute facts as returned by .../compute/get. It describes the Compute instance for which PFW rules will be managed. - @param (dict) vins_facts: dictionary with ViNS facts as returned by .../vins/get. It described ViNS - to which PFW rules set will be applied. + @param (CloudapiVinsGetResultModel) vins_model. It described ViNS to which PFW rules set will be applied. @param (list of dicts) new_rules: new PFW rules set. If None is passed, remove all existing PFW rules for the Compute. @@ -5024,12 +4588,10 @@ class DecortController(object): # # Strategy for port forwards management: # 1) obtain current port forwarding rules for the target VM - # 2) create a delta list of port forwards (rules to add and rules to remove) - # - full match between existing & requested = ignore, no update of pfw_delta - # - existing rule not present in requested list => copy to pfw_delta and mark as 'delete' - # - requested rule not present in the existing list => copy to pfw_delta and mark as 'create' - # 3) provision delta list (first delete rules marked for deletion, next add rules mark for creation) - # + # 2) index existing and new rules by (port_start, port_end, local_port, protocol) + # 3) compare key sets to find rules to add and rules to delete + # 4) provision changes (first delete, then add) + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "pfw_configure") @@ -5037,26 +4599,37 @@ class DecortController(object): if self.amodule.check_mode: self.result['failed'] = False - self.result['msg'] = ("pfw_configure() in check mode: port forwards configuration requested " - "for Compute ID {} / ViNS ID {}").format(comp_facts['id'], vins_facts['id']) - ret_rules = self._pfw_get(comp_facts['id'], vins_facts['id']) + self.result['msg'] = ( + f'pfw_configure() in check mode: port forwards ' + f'configuration requested for Compute ID {comp_facts['id']}' + f' / ViNS ID {vins_model.id}.' + ) + ret_rules = self._pfw_get(comp_facts['id'], vins_model.id) return ret_rules iface_ipaddr = "" # keep IP address associated with Compute's connection to this ViNS - need this for natRuleDel API for iface in comp_facts['interfaces']: - if iface['connType'] == 'VXLAN' and iface['connId'] == vins_facts['vxlanId']: + if ( + iface['connType'] == 'VXLAN' + and iface['connId'] == vins_model.vxlan_id + ): iface_ipaddr = iface['ipAddress'] break else: self.result['failed'] = True - self.result['msg'] = "Compute ID {} is not connected to ViNS ID {}.".format(comp_facts['id'], - vins_facts['id']) + self.result['msg'] = ( + f'Compute ID {comp_facts['id']} is not ' + f'connected to ViNS ID {vins_model.id}.' + ) return ret_rules - existing_rules = [] - for runner in vins_facts['vnfs']['NAT']['config']['rules']: - if runner['vmId'] == comp_facts['id']: - existing_rules.append(runner) + existing_rules: list[sdk_types.NATRuleAPIResultNM] = [] + if vins_model.vnfs.nat is not None: + for runner in vins_model.vnfs.nat.config.rules: + if runner.vm_id == comp_facts['id']: + existing_rules.append(runner) + else: + raise RuntimeError('VINS NAT VNF must exist.') if not existing_rules and not new_rules: self.result['failed'] = False @@ -5071,105 +4644,80 @@ class DecortController(object): arg_req_function=requests.post, arg_api_name="/restmachine/cloudapi/vins/natRuleDel", arg_params={ - 'vinsId': vins_facts['id'], - 'ruleId': rule['id'] + 'vinsId': vins_model.id, + 'ruleId': rule.id } - ) + ) self.result['changed'] = True return ret_rules - # - # delta_list will be a list of dictionaries that describe _changes_ to the port forwarding rules - # of the Compute in hands. - # The dictionary has the following keys - values: - # (int) publicPortStart - external port range start - # (int) publicPortEnd - external port range end - # (int) localPort - internal port number - # (string) protocol - protocol, either 'tcp' or 'udp' - # (string) action - string, either 'del' or 'add' - # (int) id - the ID of existing PFW rule that should be deleted (applicable only for action='del') - # - delta_list = [] - # select from new_rules the rules to add - those not found in existing rules + existing_rule_by_key = { + ( + rule.public_port_start, + rule.public_port_end, + rule.local_port, + rule.protocol.value + ): rule + for rule in existing_rules + } + new_rule_by_key = {} for rule in new_rules: - rule['action'] = 'add' - rule_port_end = rule.get('public_port_end', rule['public_port_start']) - if rule_port_end > rule['public_port_start']: - # This is a ranged rule, i.e. when range of public ports maps to an equally - # sized range of local ports. - # For such case we have to make sure that the local port equals public - # port (this check & adjustment will be made by vnf_nat.add method anyway, but - # if we adjust here, we can avoid unnecessary rule del / add iteration in the - # module run, thus saving execution time) - rule['local_port'] = rule['public_port_start'] - for runner in existing_rules: - if (runner['publicPortStart'] == rule['public_port_start'] and - runner['publicPortEnd'] == rule_port_end and - runner['localPort'] == rule['local_port'] and - runner['protocol'] == rule['proto']): - rule['action'] = 'keep' - break - if rule['action'] == 'add': - delta_rule = dict(publicPortStart=rule['public_port_start'], - publicPortEnd=rule_port_end, - localPort=rule['local_port'], - protocol=rule['proto'], - action='add', - id='-1') - delta_list.append(delta_rule) + port_end = rule.get('public_port_end', rule['public_port_start']) + local_port = ( + rule['public_port_start'] + if port_end > rule['public_port_start'] + else rule['local_port'] + ) + new_rule_by_key[ + ( + rule['public_port_start'], + port_end, + local_port, + rule['proto'], + ) + ] = rule - # select from existing_rules the rules to delete - those not found in new_rules - for rule in existing_rules: - rule['action'] = 'del' - for runner in new_rules: - runner_port_end = runner.get('public_port_end', runner['public_port_start']) - if (rule['publicPortStart'] == runner['public_port_start'] and - rule['publicPortEnd'] == runner_port_end and - rule['localPort'] == runner['local_port'] and - rule['protocol'] == runner['proto']): - rule['action'] = 'keep' - break - if rule['action'] == 'del': - delta_list.append(rule) - - if not len(delta_list): - # strange, but still nothing to do? + rules_to_delete = [ + existing_rule_by_key[k] + for k in existing_rule_by_key.keys() - new_rule_by_key.keys() + ] + rules_to_add = new_rule_by_key.keys() - existing_rule_by_key.keys() + if not rules_to_delete and not rules_to_add: self.result['failed'] = False - self.result['warning'] = ("pfw_configure() no difference between current and new PFW rules " - "found. No change applied to Compute ID {}.").format(comp_facts['id']) - ret_rules = self._pfw_get(comp_facts['id'], vins_facts['id']) + self.result['warning'] = ( + f'pfw_configure() no difference between current and new ' + f'PFW rules found. No change applied to Compute ID ' + f'{comp_facts['id']}.' + ) + ret_rules = self._pfw_get(comp_facts['id'], vins_model.id) return ret_rules - # now delta_list contains a list of enriched rule dictionaries with extra key 'action', which - # tells what kind of action is expected on this rule - 'add' or 'del' - # We first iterate to delete, then iterate again to add rules - - # Sort pfw_delta_list so that the items with action="del" come first, and those with - # action='add' come last. - # Iterate over pfw_delta_list and first delete port forwarding rules marked for deletion, - # next create the rules marked for creation. api_base = "/restmachine/cloudapi/vins/" - for delta_rule in sorted(delta_list, key=lambda i: i['action'], reverse=True): - if delta_rule['action'] == 'del': - api_params = dict(vinsId=vins_facts['id'], - ruleId=delta_rule['id']) - self.decort_api_call(requests.post, api_base + 'natRuleDel', api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['changed'] = True - elif delta_rule['action'] == 'add': - api_params = dict(vinsId=vins_facts['id'], - intIp=iface_ipaddr, - intPort=delta_rule['localPort'], - extPortStart=delta_rule['publicPortStart'], - extPortEnd=delta_rule['publicPortEnd'], - proto=delta_rule['protocol']) - self.decort_api_call(requests.post, api_base + 'natRuleAdd', api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['changed'] = True + + for rule in rules_to_delete: + self.decort_api_call( + requests.post, + api_base + 'natRuleDel', + dict(vinsId=vins_model.id, ruleId=rule.id) + ) + self.result['changed'] = True + + for port_start, port_end, local_port, protocol in rules_to_add: + self.decort_api_call( + requests.post, + api_base + 'natRuleAdd', + dict(vinsId=vins_model.id, + intIp=iface_ipaddr, + intPort=local_port, + extPortStart=port_start, + extPortEnd=port_end, + proto=protocol) + ) + self.result['changed'] = True self.result['failed'] = False - ret_rules = self._pfw_get(comp_facts['id'], vins_facts['id']) + ret_rules = self._pfw_get(comp_facts['id'], vins_model.id) return ret_rules ############################## # @@ -5185,23 +4733,12 @@ class DecortController(object): to find the k8s with the specified ID, it may return 0 for ID and empty dictionary for the facts. So it is suggested to check the return values accordingly. """ - ret_k8s_id = 0 - ret_k8s_dict = dict() - if not k8s_id: self.result['failed'] = True self.result['msg'] = "k8s_get_by_id(): zero k8s ID specified." self.amodule.fail_json(**self.result) - api_params = dict(k8sId=k8s_id, ) - api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/get", api_params) - if api_resp.status_code == 200: - ret_k8s_dict = json.loads(api_resp.content.decode('utf8')) - else: - self.result['warning'] = ("k8s_get_by_id(): failed to get k8s by ID {}. HTTP code {}, " - "response {}.").format(k8s_id, api_resp.status_code, api_resp.reason) - - return ret_k8s_dict + return self.api.ca.k8s.get(k8s_id=k8s_id).model_dump() def k8s_find(self, k8s_id, k8s_name="",rg_id=0,check_state=True): """Returns non zero k8s ID and a dictionary with k8s details on success, 0 and empty dictionary otherwise. @@ -5267,58 +4804,40 @@ class DecortController(object): VALID_TARGET_STATES = ["ENABLED", "DISABLED"] if arg_k8s_dict['status'] in NOP_STATES_FOR_K8S_CHANGE: - self.result['failed'] = False self.result['msg'] = ("k8s_state(): no state change possible for k8s ID {} " "in its current state '{}'.").format(arg_k8s_dict['id'], arg_k8s_dict['status']) return if arg_k8s_dict['status'] not in VALID_TARGET_STATES: - self.result['failed'] = False self.result['warning'] = ("k8s_state(): unrecognized desired state '{}' requested " "for k8s ID {}. No k8s state change will be done.").format(arg_desired_state, arg_k8s_dict['id']) return - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = ("k8s_state() in check mode: setting state of k8s ID {}, name '{}' to " - "'{}' was requested.").format(arg_k8s_dict['id'], arg_k8s_dict['name'], - arg_desired_state) - return - - k8s_state_api = "" # This string will also be used as a flag to indicate that API call is necessary - api_params = dict(k8sId=arg_k8s_dict['id']) - expected_state = "" - tech_state = "" + sdk_func = None if arg_k8s_dict['status'] in ['CREATED', 'ENABLED']: if arg_desired_state == 'disabled': - k8s_state_api = '/restmachine/cloudapi/k8s/disable' - expected_state = 'DISABLED' + sdk_func = self.api.ca.k8s.disable elif ( - arg_k8s_dict['techStatus'] == 'STARTED' + arg_k8s_dict['tech_status'] == 'STARTED' and arg_desired_state == 'stopped' ): - k8s_state_api = '/restmachine/cloudapi/k8s/stop' + sdk_func = self.api.ca.k8s.stop elif ( - arg_k8s_dict['techStatus'] == 'STOPPED' + arg_k8s_dict['tech_status'] == 'STOPPED' and arg_desired_state == 'started' ): - k8s_state_api = '/restmachine/cloudapi/k8s/start' + sdk_func = self.api.ca.k8s.start elif ( arg_k8s_dict['status'] == 'DISABLED' and arg_desired_state == 'enabled' ): - k8s_state_api = '/restmachine/cloudapi/k8s/enable' - expected_state = 'ENABLED' + sdk_func = self.api.ca.k8s.enable - if k8s_state_api != "": - self.decort_api_call(requests.post, k8s_state_api, api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['changed'] = True - arg_k8s_dict['status'] = expected_state - arg_k8s_dict['started'] = tech_state + if sdk_func is not None: + self.sdk_checkmode(sdk_func)( + k8s_id=arg_k8s_dict['id'], + ) else: - self.result['failed'] = False self.result['msg'] = ("k8s_state(): no state change required for k8s ID {} from current " "state '{}' to desired state '{}'.").format(arg_k8s_dict['id'], arg_k8s_dict['status'], @@ -5326,66 +4845,25 @@ class DecortController(object): return - def k8s_delete(self, k8s_id, permanently=False): - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_delete") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = "k8s_delete() in check mode: delete K8s cluster ID {} was requested.".format(k8s_id) - return - - api_params = dict(k8sId=k8s_id, - permanently=permanently, - ) - self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/delete", api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['msg'] = "k8s_delete() K8s cluster ID {} was deleted.".format(k8s_id) - self.result['changed'] = True - return - - def k8s_restore(self, k8s_id ): - """Restores a deleted k8s cluster identified by ID. - - @param k8s_id: ID of the k8s cluster to restore. - """ - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_restore") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = "k8s_restore() in check mode: restore k8s ID {} was requested.".format(k8s_id) - return - - api_params = dict(k8sId=k8s_id) - self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/restore", api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['changed'] = True - return - - def k8s_enable(self,k8s_id): - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_enable") - api_params = dict(k8sId=k8s_id) - api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/enable", api_params) - self.result['failed'] = False - self.result['changed'] = True - return - def k8s_check_worker_group_for_recreate( self, target_wg: dict[str, Any], existing_wg: dict[str, Any], ): - for param in [ - 'cpu', 'ram', 'disk', 'taints', 'labels', 'annotations', - ]: + worker_group_param_to_sdk_field = { + 'cpu': 'node_cpu_count', + 'ram': 'node_ram_size_mb', + 'disk': 'node_boot_disk_size_gb', + 'taints': 'taints', + 'labels': 'labels', + 'annotations': 'annotations', + } + for param, sdk_field in worker_group_param_to_sdk_field.items(): # Ignore service label when comparing labels if param == 'labels': if target_wg[param] is not None: filtered_existing_wg_labels = [ - label for label in existing_wg[param] + label for label in existing_wg[sdk_field] if 'workersGroupName' not in label ] if ( @@ -5395,7 +4873,7 @@ class DecortController(object): target_wg['need_to_recreate'] = True elif ( target_wg[param] is not None - and existing_wg[param] != target_wg[param] + and existing_wg[sdk_field] != target_wg[param] ): target_wg['need_to_recreate'] = True @@ -5404,12 +4882,12 @@ class DecortController(object): and not target_wg.get('need_to_recreate') ): _, vm_info, _ = self._compute_get_by_id( - comp_id=existing_wg['detailedInfo'][0]['id'], + comp_id=existing_wg['vms'][0]['id'], ) - if ( - vm_info.get('userdata', {}) - != target_wg['ci_user_data'] - ): + existing_userdata = vm_info.get('userdata', {}) + if isinstance(existing_userdata, str): + existing_userdata = json.loads(existing_userdata) + if existing_userdata != target_wg['ci_user_data']: target_wg['need_to_recreate'] = True def k8s_provision( @@ -5554,7 +5032,7 @@ class DecortController(object): self.result['msg'] = result_msg return - if self.k8s_info['techStatus'] != "STARTED": + if self.k8s_info['tech_status'] != "STARTED": self.result['msg'] = ("k8s_workers_modify(): Can't modify with TechStatus other then STARTED") return @@ -5563,9 +5041,9 @@ class DecortController(object): wg_modadd_list = [] wg_moddel_list = [] wg_outer = [rec['name'] for rec in arg_modwg] - wg_inner = [rec['name'] for rec in arg_k8swg['k8sGroups']['workers']] + wg_inner = [rec['name'] for rec in arg_k8swg['node_groups']['worker']] - for rec in arg_k8swg['k8sGroups']['workers']: + for rec in arg_k8swg['node_groups']['worker']: if rec['name'] not in wg_outer: wg_del_list.append(rec['id']) @@ -5573,7 +5051,7 @@ class DecortController(object): if rec['name'] not in wg_inner: wg_add_list.append(rec) - for wg in arg_k8swg['k8sGroups']['workers']: + for wg in arg_k8swg['node_groups']['worker']: for target_wg in arg_modwg: if wg['name'] == target_wg['name']: self.k8s_check_worker_group_for_recreate( @@ -5583,20 +5061,27 @@ class DecortController(object): if target_wg.get('need_to_recreate'): wg_del_list.append(wg['id']) wg_to_create = deepcopy(target_wg) + param_to_sdk_field = { + 'num': 'node_count', + 'cpu': 'node_cpu_count', + 'ram': 'node_ram_size_mb', + 'disk': 'node_boot_disk_size_gb', + } for param, value in wg_to_create.items(): if param == 'ci_user_data' and value is None: _, vm_info, _ = self._compute_get_by_id( - comp_id=wg['detailedInfo'][0]['id'], + comp_id=wg['vms'][0]['id'], ) wg_to_create[param] = vm_info.get( 'userdata', {} ) elif value is None: - wg_to_create[param] = wg.get(param) + sdk_field = param_to_sdk_field.get(param, param) + wg_to_create[param] = wg.get(sdk_field) wg_add_list.append(wg_to_create) continue - w_ids = {w['id'] for w in wg['detailedInfo']} + w_ids = {w['id'] for w in wg['vms']} bad_w_ids = set() new_chipset = target_wg['chipset'] @@ -5608,7 +5093,7 @@ class DecortController(object): if vm_info['chipset'] != new_chipset: bad_w_ids.add(w_id) - wg_num = wg['num'] + wg_num = wg['node_count'] target_num = target_wg['num'] if target_num is not None: new_w_count = target_num - wg_num + len(bad_w_ids) @@ -5756,23 +5241,6 @@ class DecortController(object): self.result['failed'] = False return - @waypoint - def k8s_update(self, *, id: int, name: str): - - api_params = { - 'k8sId': id, - 'name': name, - } - - self.decort_api_call( - arg_req_function=requests.post, - arg_api_name='/restmachine/cloudapi/k8s/update', - arg_params=api_params, - ) - - self.set_changed() - return - def k8s_k8ci_find(self,arg_k8ci_id): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_k8ci_find") @@ -5822,9 +5290,8 @@ class DecortController(object): return api_response.json() def k8s_get_master_node_storage_policy_id(self, k8s_info: dict) -> int: - master_nodes_info = k8s_info['k8sGroups']['masters'][ - 'detailedInfo' - ] + master = k8s_info['node_groups']['master'] + master_nodes_info = master.get('vms') or master.get('detailedInfo', []) if not master_nodes_info: raise ValueError( f'No master nodes found in K8s cluster ID {k8s_info['id']}' @@ -5889,32 +5356,6 @@ class DecortController(object): else: return bservice_id,None - def bservice_provision(self,bs_name,rgid,sshuser=None,sshkey=None, zone_id: None | int = None): - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "bservice_provision") - - if self.amodule.check_mode: - result_msg = 'bservice_provision() in check mode: No changing.' - if self.result.get('msg'): - self.result['msg'] += f'\n{result_msg}' - else: - self.result['msg'] = result_msg - return 0 - - api_url = "/restmachine/cloudapi/bservice/create" - api_params = dict( - name = bs_name, - rgId = rgid, - sshUser = sshuser, - sshKey = sshkey, - zoneId=zone_id, - ) - api_resp = self.decort_api_call(requests.post, api_url, api_params) - self.result['failed'] = False - self.result['changed'] = True - ret_bservice_id = int(api_resp.content.decode('utf8')) - return ret_bservice_id - def bservice_state(self,bs_dict,desired_state): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "bservice_state") @@ -5923,7 +5364,6 @@ class DecortController(object): VALID_TARGET_STATES = ["enabled", "disabled", 'started', 'stopped', 'present'] if bs_dict['status'] in NOP_STATES_FOR_BS_CHANGE: - self.result['failed'] = False self.result['msg'] = ("bservice_state(): no state change possible for ViNS ID {} " "in its current state '{}'.").format(bs_dict['id'], bs_dict['status']) return @@ -5932,57 +5372,42 @@ class DecortController(object): desired_state is not None and desired_state not in VALID_TARGET_STATES ): - self.result['failed'] = False self.result['warning'] = ("bservice_state(): unrecognized desired state '{}' requested " "for B-service ID {}. No B-service state change will be done.").format(desired_state, bs_dict['id']) return - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = ("bservice_state() in check mode: setting state of B-service ID {}, name '{}' to " - "'{}' was requested.").format(bs_dict['id'], bs_dict['name'], - desired_state) - return - - bsstate_api = "" # this string will also be used as a flag to indicate that API call is necessary - api_params = dict(serviceId=bs_dict['id']) - expected_state = "" + sdk_func = None if bs_dict['status'] == 'CREATED': if desired_state == 'disabled': - bsstate_api = '/restmachine/cloudapi/bservice/disable' + sdk_func = self.api.ca.bservice.disable elif desired_state == 'enabled': - bsstate_api = '/restmachine/cloudapi/bservice/enable' + sdk_func = self.api.ca.bservice.enable if bs_dict['status'] == 'ENABLED': if desired_state == 'disabled': - bsstate_api = '/restmachine/cloudapi/bservice/disable' - expected_state = 'DISABLED' + sdk_func = self.api.ca.bservice.disable elif ( bs_dict['techStatus'] == 'STARTED' and desired_state == 'stopped' ): - bsstate_api = '/restmachine/cloudapi/bservice/stop' + sdk_func = self.api.ca.bservice.stop elif ( bs_dict['techStatus'] == 'STOPPED' and desired_state == 'started' ): - bsstate_api = '/restmachine/cloudapi/bservice/start' + sdk_func = self.api.ca.bservice.start elif ( bs_dict['status'] == 'DISABLED' and desired_state == 'enabled' ): - bsstate_api = '/restmachine/cloudapi/bservice/enable' - expected_state = 'ENABLED' + sdk_func = self.api.ca.bservice.enable - if bsstate_api != "": - self.decort_api_call(requests.post, bsstate_api, api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['changed'] = True - bs_dict['status'] = expected_state + if sdk_func is not None: + self.sdk_checkmode(sdk_func)( + bservice_id=bs_dict['id'], + ) else: - self.result['failed'] = False self.result['msg'] = ("bservice_state(): no state change required for B-service ID {} from current " "state '{}' to desired state '{}'.").format(bs_dict['id'], bs_dict['status'], @@ -5990,24 +5415,6 @@ class DecortController(object): return - def bservice_delete(self,bs_id,permanently=True): - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "bservice_delete") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = "bservice_delete() in check mode: delete B-Service ID {} was requested.".format(bs_id) - return - - api_params = dict(serviceId=bs_id,permanently=permanently) - self.decort_api_call(requests.post, "/restmachine/cloudapi/bservice/delete", api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['msg'] = "bservice_delete() B-Service ID {} was deleted.".format(bs_id) - self.result['changed'] = True - - return - @waypoint @checkmode def bservice_migrate_to_zone(self, bs_id: int, zone_id: int): @@ -6228,43 +5635,40 @@ class DecortController(object): #################### ### LB MANAGMENT ### #################### - def _lb_get_by_id(self,lb_id): + @handle_sdk_exceptions + def _lb_get_by_id(self, lb_id: int) -> sdk_types.CloudapiLbGetResultModel: """Helper function that locates LB by ID and returns LB facts. This function expects that the ViNS exists (albeit in DELETED or DESTROYED state) and will return 0 LB ID if not found. - @param (int) vins_id: ID of the LB to find and return facts for. + @param (int) lb_id: ID of the LB to find and return facts for. - @return: LB ID and a dictionary of LB facts as provided by LB/get API call. + @return: LB model. """ - ret_lb_id = 0 - ret_lb_dict = dict() if not lb_id: - self.result['failed'] = True - self.result['msg'] = "lb_get_by_id(): zero LB ID specified." - self.amodule.fail_json(**self.result) + self.message('lb_get_by_id(): zero LB ID specified.') + self.exit(fail=True) - api_params = dict(lbId=lb_id) - api_resp = self.decort_api_call( - requests.post, - '/restmachine/cloudapi/lb/get', - api_params, - not_fail_codes=[404] - ) - if api_resp.status_code == 200: - ret_lb_id = lb_id - ret_lb_dict = json.loads(api_resp.content.decode('utf8')) - elif api_resp.status_code == 404: - self.message(f'LB with ID {lb_id} not found.') - self.exit(fail=True) - else: - self.message( - f'lb_get_by_id(): failed to get LB by ID {lb_id}. ' - f'HTTP code {api_resp.status_code}, response {api_resp.reason}.' + try: + lb_model = self.api.cloudapi.lb.get( + lb_id=lb_id ) - self.exit(fail=True) - return ret_lb_id, ret_lb_dict + return lb_model + 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='lb', + id=lb_id, + ) + ) + self.exit(fail=True) + else: + raise e def _rg_listlb(self,rg_id): """List all LB in the resource group @@ -6285,24 +5689,41 @@ class DecortController(object): return [] return ret_rg_vins_list['data'] + + @handle_sdk_exceptions def lb_find(self,lb_id=0,lb_name="",rg_id=0): """Find specified LB. @returns: LB ID and dictionary with LB facts. """ - LB_INVALID_STATES = ["ENABLING", "DISABLING", "DELETING", "DESTROYING", "DESTROYED"] - - api_params = dict() - ret_lb_id = 0 - ret_lb_facts = None - + LB_INVALID_STATES = [ + sdk_types.LBStatus.DELETING, + sdk_types.LBStatus.DESTROYED, + sdk_types.LBStatus.DESTROYING, + sdk_types.LBStatus.DISABLING, + sdk_types.LBStatus.ENABLING, + ] self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_find") if lb_id > 0: - ret_lb_id, ret_lb_facts = self._lb_get_by_id(lb_id) - - if not self.amodule.check_mode or ret_lb_facts['status'] not in LB_INVALID_STATES: - return ret_lb_id, ret_lb_facts + try: + ret_lb_model = self.api.cloudapi.lb.get( + lb_id=lb_id, + ) + except sdk_exceptions.RequestException as e: + if ( + e.orig_exception.response is not None + and e.orig_exception.response.status_code == 404 + ): + return 0, None + else: + raise e + ret_lb_id = ret_lb_model.id + if ( + not self.amodule.check_mode + or ret_lb_model.status not in LB_INVALID_STATES + ): + return ret_lb_id, ret_lb_model else: return 0, None elif lb_name != "": @@ -6310,9 +5731,13 @@ class DecortController(object): list_lb = self._rg_listlb(rg_id) for lb in list_lb: if lb['name'] == lb_name: - ret_lb_id, ret_lb_facts = self._lb_get_by_id(lb['id']) - if not self.amodule.check_mode or ret_lb_facts['status'] not in LB_INVALID_STATES: - return ret_lb_id, ret_lb_facts + ret_lb_model = self.api.cloudapi.lb.get(lb_id=lb['id']) + if ( + not self.amodule.check_mode + or ret_lb_model.status not in LB_INVALID_STATES + ): + ret_lb_id = ret_lb_model.id + return ret_lb_id, ret_lb_model else: return 0, None else: @@ -6326,6 +5751,7 @@ class DecortController(object): self.amodule.fail_json(**self.result) return 0, None + def lb_provision( self, lb_name, @@ -6378,117 +5804,79 @@ class DecortController(object): self.result['changed'] = True ret_lb_id = int(api_resp.content.decode('utf8')) return ret_lb_id - def lb_delete(self,lb_id,permanently=False): - """Deletes specified LB. - @param (int) lb_id: integer value that identifies the ViNS to be deleted. - @param (bool) permanently: a bool that tells if deletion should be permanent. If False, the LB will be - marked as DELETED and placed into a trash bin for predefined period of time (usually, a few days). Until - this period passes this LB can be restored by calling the corresponding 'restore' method. - """ - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_delete") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = "lb_delete() in check mode: delete ViNS ID {} was requested.".format(lb_id) - return - - api_params = dict(lbId=lb_id, - permanently=permanently) - self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/delete", api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['changed'] = True - return lb_id - - def lb_state(self, lb_dict, desired_state): + def lb_state( + self, + lb_model: sdk_types.CloudapiLbGetResultModel, + desired_state: str, + ): """Change state for LB. """ self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_state") - NOP_STATES_FOR_LB_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING", - "DESTROYED"] + NOP_STATES_FOR_LB_CHANGE = [ + sdk_types.LBStatus.DELETED, + sdk_types.LBStatus.DELETING, + sdk_types.LBStatus.DESTROYED, + sdk_types.LBStatus.DESTROYING, + sdk_types.LBStatus.DISABLING, + sdk_types.LBStatus.ENABLING, + sdk_types.LBStatus.MODELED, + ] VALID_TARGET_STATES = ["enabled", "disabled","restart", 'started', 'stopped'] - VALID_TARGET_TSTATES = ["STARTED","STOPPED"] - if lb_dict['status'] in NOP_STATES_FOR_LB_CHANGE: - self.result['failed'] = False - self.result['msg'] = ("lb_state(): no state change possible for LB ID {} " - "in its current state '{}'.").format(lb_dict['id'], lb_dict['status']) + if lb_model.status in NOP_STATES_FOR_LB_CHANGE: + self.result['msg'] = ( + f'lb_state(): no state change possible for LB ID ' + f'{lb_model.id} in its current state "{lb_model.status.name}"' + ) return if ( desired_state is not None and desired_state not in VALID_TARGET_STATES ): - self.result['failed'] = False - self.result['warning'] = ("lb_state(): unrecognized desired state '{}' requested " - "for LB ID {}. No LB state change will be done.").format(desired_state, - lb_dict['id']) + self.result['warning'] = ( + f'lb_state(): unrecognized desired state ' + f'"{desired_state}" requested for LB ID {lb_model.id}. ' + f'No LB state change will be done.' + ) return - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = ("lb_state() in check mode: setting state of LB ID {}, name '{}' to " - "'{}' was requested.").format(lb_dict['id'], lb_dict['name'], - desired_state) - return - - state_api = "" - api_params = dict(lbId=lb_dict['id']) - expected_state = "" - if lb_dict['status'] in ["CREATED", "ENABLED"]: + sdk_func = None + if lb_model.status in [ + sdk_types.LBStatus.CREATED, + sdk_types.LBStatus.ENABLED, + ]: if desired_state == 'disabled': - state_api = "/restmachine/cloudapi/lb/disable" - expected_state = "DISABLED" - if lb_dict['techStatus'] == "STARTED": + sdk_func = self.api.ca.lb.disable + if lb_model.tech_status == sdk_types.LBTechStatus.STARTED: if desired_state == 'stopped': - state_api = "/restmachine/cloudapi/lb/stop" + sdk_func = self.api.ca.lb.stop if desired_state == 'restart': - state_api = "/restmachine/cloudapi/lb/restart" - elif lb_dict['techStatus'] == "STOPPED": + sdk_func = self.api.ca.lb.restart + elif lb_model.tech_status == sdk_types.LBTechStatus.STOPPED: if desired_state == 'started': - state_api = "/restmachine/cloudapi/lb/start" - elif lb_dict['status'] == "DISABLED" and desired_state == 'enabled': - state_api = "/restmachine/cloudapi/lb/enable" - expected_state = "ENABLED" + sdk_func = self.api.ca.lb.start + elif ( + lb_model.status == sdk_types.LBStatus.DISABLED + and desired_state == 'enabled' + ): + sdk_func = self.api.ca.lb.enable - if state_api != "": - self.decort_api_call(requests.post, state_api, api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['changed'] = True - lb_dict['status'] = expected_state + if sdk_func is not None: + self.sdk_checkmode(sdk_func)( + lb_id=lb_model.id, + ) elif desired_state is not None: - self.result['failed'] = False - self.result['msg'] = ("lb_state(): no state change required for LB ID {} from current " - "state '{}' to desired state '{}'.").format(lb_dict['id'], - lb_dict['status'], - desired_state) + self.result['msg'] = ( + f'lb_state(): no state change required for LB ID ' + f'{lb_model.id} from current state "{lb_model.status.name}" ' + f'to desired state "{desired_state}".' + ) return - def lb_restore(self, lb_id): - """Restores previously deleted LB identified by its ID. - @param lb_id: ID of the LB to restore. - - @returns: nothing on success. On error this method will abort module execution. - """ - - self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_restore") - - if self.amodule.check_mode: - self.result['failed'] = False - self.result['msg'] = "lb_restore() in check mode: restore LB ID {} was requested.".format(lb_id) - return - - api_params = dict(lbId=lb_id) - self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/restore", api_params) - # On success the above call will return here. On error it will abort execution by calling fail_json. - self.result['failed'] = False - self.result['changed'] = True - return def lb_update_backends(self,lb_backends,mod_backends,mod_servers): """ @@ -6703,7 +6091,7 @@ class DecortController(object): def lb_update( self, - lb_facts: dict, + lb_model: sdk_types.CloudapiLbGetResultModel, aparam_backends: list | None, aparam_frontends: list | None, aparam_servers: list | None, @@ -6720,14 +6108,15 @@ class DecortController(object): self.result['msg'] = result_msg return - lb_backends = lb_facts['backends'] - lb_frontends = lb_facts['frontends'] + lb_backends = lb_model.backends + lb_frontends = lb_model.frontends + del_list_backs: set[str] = set() if aparam_backends is None: - upd_back_list = [back['name'] for back in lb_backends] + upd_back_list = [back.name for back in lb_backends] else: #lists from module and cloud mod_backs_list = [back['name'] for back in aparam_backends] - lb_backs_list = [back['name'] for back in lb_backends] + lb_backs_list = [back.name for back in lb_backends] #ADD\DEL\UPDATE LISTS OF BACKENDS del_list_backs = set(lb_backs_list).difference(mod_backs_list) add_back_list = set(mod_backs_list).difference(lb_backs_list) @@ -6760,17 +6149,20 @@ class DecortController(object): if aparam_frontends is not None: mod_front_list = [front['name'] for front in aparam_frontends] - lb_front_list = [front['name'] for front in lb_frontends] + lb_front_list = [front.name for front in lb_frontends] del_list_fronts = set(lb_front_list).difference(mod_front_list) add_list_fronts = set(mod_front_list).difference(lb_front_list) upd_front_list = set(lb_front_list).intersection(mod_front_list) + if del_list_backs: + already_deleted = {f.name for f in lb_frontends if f.backend_name in del_list_backs} + del_list_fronts -= already_deleted if del_list_fronts: self._lb_delete_fronts(del_list_fronts) - front_ha_ip = lb_facts['frontendHAIP'] - back_ha_ip = lb_facts['backendHAIP'] + front_ha_ip = lb_model.frontend_ha_ip_addr + back_ha_ip = lb_model.backend_ha_ip_addr #set bind_ip if front_ha_ip != "": bind_ip = front_ha_ip @@ -6779,11 +6171,11 @@ class DecortController(object): bind_ip = back_ha_ip if front_ha_ip == "" and back_ha_ip == "": - prime = lb_facts['primaryNode'] - if prime["frontendIp"] != "": - bind_ip = prime["frontendIp"] + prime = lb_model.primary_node + if prime.frontend_ip_addr != "": + bind_ip = prime.frontend_ip_addr else: - bind_ip = prime["backendIp"] + bind_ip = prime.backend_ip_addr if add_list_fronts: self._lb_add_fronts(add_list_fronts,aparam_frontends,bind_ip) @@ -6792,20 +6184,24 @@ class DecortController(object): if aparam_sysctl is not None: sysctl_with_str_values = {k: str(v) for k, v in aparam_sysctl.items()} - if sysctl_with_str_values != lb_facts['sysctlParams']: + if sysctl_with_str_values != lb_model.sysctl_params: self.lb_update_sysctl( - lb_id=lb_facts['id'], + lb_id=lb_model.id, sysctl=aparam_sysctl, ) return - def _lb_delete_backends(self,back_list,lb_fronts): + def _lb_delete_backends( + self, + back_list: set[str], + lb_fronts: list[sdk_types.LBFrontendAPIResultNM], + ): #delete frontends with that backend self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_delete_backends") for back in back_list: - fronts = list(filter(lambda i: i['backend'] == back,lb_fronts)) + fronts = list(filter(lambda i: i.backend_name == back,lb_fronts)) if fronts: self._lb_delete_fronts(fronts) api_params = dict( @@ -6815,23 +6211,27 @@ class DecortController(object): api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendDelete", api_params) self.result['changed'] = True return - def _lb_delete_fronts(self,d_fronts): + def _lb_delete_fronts( + self, + d_fronts: set[str] | list[sdk_types.LBFrontendAPIResultNM], + ): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_delete_fronts") for front in d_fronts: api_params = dict( lbId=self.lb_id, - frontendName=front['name'] if "name" in front else front, + frontendName=front.name if not isinstance(front, str) else front, ) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/frontendDelete", api_params) - #del from cloud dict - if type(front)==dict: - self.lb_facts['frontends'].remove(front) self.result['changed'] = True - return - def _lb_add_fronts(self,front_list,mod_fronts,bind_ip): + def _lb_add_fronts( + self, + front_list: set[str], + mod_fronts: list[dict], + bind_ip: str, + ): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_add_fronts") @@ -6881,50 +6281,58 @@ class DecortController(object): address = server['address'], port = srv_back['port'], check = srv_back['check'] if "check" in srv_back else None, - **srv_back['server_settings'] if "server_settings" in srv_back else {}, - ) + **srv_back.get( + 'server_settings', self.default_settings + ), + ) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendServerAdd", api_params) self.result['changed'] = True return - def _lb_update_backends(self,back_list,lb_backs,mod_backs,mod_serv): + def _lb_update_backends( + self, + back_list: set[str] | list[str], + lb_backs: list[sdk_types.LBBackendAPIResultNM], + mod_backs: list[dict], + mod_serv: list[dict], + ): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_update_backends") - lb_backs = list(filter(lambda i: i['name'] in back_list,lb_backs)) + lb_backs = list(filter(lambda i: i.name in back_list,lb_backs)) for back in lb_backs: - - del back['serverDefaultSettings']['guid'] - mod_back, = list(filter(lambda i: i['name']==back['name'],mod_backs)) + mod_back, = list( + filter(lambda i: i['name'] == back.name,mod_backs) + ) if "default_settings" not in mod_back: mod_back["default_settings"] = self.default_settings else: for param,value in self.default_settings.items(): if param not in mod_back["default_settings"]: - mod_back["default_settings"].update(param,value) - + mod_back["default_settings"][param] = value if "algorithm" not in mod_back: mod_back["algorithm"] = self.default_alg - - if back['serverDefaultSettings'] != mod_back["default_settings"] or\ - mod_back["algorithm"] != back['algorithm']: + back_sds = back.server_default_settings.model_dump( + exclude={'guid'} + ) + if back_sds != mod_back["default_settings"] or\ + mod_back["algorithm"] != back.algorithm: api_params = dict( lbId=self.lb_id, - backendName = back['name'], - algorithm = mod_back['algorithm'], + backendName=back.name, + algorithm=mod_back['algorithm'], **mod_back['default_settings'], ) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendUpdate", api_params) self.result['changed'] = True - - lb_servers_list = [srv['name'] for srv in back['servers']] + lb_servers_list = [srv.name for srv in back.servers] for server in mod_serv: try: - mod_back, = list(filter(lambda i: i['name'] == back['name'],server['backends'])) + mod_back, = list(filter(lambda i: i['name'] == back.name,server['backends'])) # noqa: 501 except: continue if server['name'] not in lb_servers_list: @@ -6935,27 +6343,30 @@ class DecortController(object): address = server['address'], port = mod_back['port'], check = mod_back['check'] if "check" in mod_back else None, - **mod_back['server_settings'] if "server_settings" in mod_back else {}, - ) + **mod_back.get( + 'server_settings', self.default_settings + ), + ) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendServerAdd", api_params) self.result['changed'] = True else: - lb_server, = list(filter(lambda i: i['name'] == server['name'],back['servers'])) - del lb_server['serverSettings']['guid'] + lb_server, = list(filter(lambda i: i.name == server['name'],back.servers)) # noqa: 501 if "server_settings" not in mod_back: - mod_back['server_settings'] = self.default_settings + mod_back['server_settings'] = self.default_settings else: for param,value in self.default_settings.items(): if param not in mod_back["server_settings"]: - mod_back["server_settings"].update(param,value) - + mod_back["server_settings"][param] = value if "check" not in mod_back: mod_back['check'] = self.default_server_check - if (server['address'] != lb_server['address'] or\ - lb_server['check']!=mod_back['check']) or\ - mod_back['server_settings'] != lb_server['serverSettings']: + lb_server_settings = lb_server.server_settings.model_dump( + exclude={'guid'}, + ) + if (server['address'] != lb_server.ip_addr or + lb_server.check != mod_back['check'] or + mod_back['server_settings'] != lb_server_settings): api_params = dict( lbId=self.lb_id, backendName = mod_back['name'], @@ -6969,21 +6380,21 @@ class DecortController(object): self.result['changed'] = True lb_servers_list.remove(server['name']) for server in lb_servers_list: - api_params = dict(lbId=self.lb_id,backendName = back['name'],serverName=server) + api_params = dict(lbId=self.lb_id,backendName=back.name,serverName=server) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendServerDelete", api_params) - self.result['changed'] = True + self.result['changed'] = True return - def _lb_update_fronts(self,upd_front_list,lb_frontends,mod_frontends,bind_ip): + def _lb_update_fronts(self, + upd_front_list: set[str],lb_frontends: list[sdk_types.LBFrontendAPIResultNM],mod_frontends: list[dict],bind_ip: str): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_update_fronts") for front in upd_front_list: mod_front, = list(filter(lambda i: i['name'] == front,mod_frontends)) - lb_front, = list(filter(lambda i: i['name'] == front,lb_frontends)) - lb_binds_list = [bind['name'] for bind in lb_front['bindings']] + lb_front, = list(filter(lambda i: i.name == front,lb_frontends)) + lb_binds_list = [bind.name for bind in lb_front.bindings] for bind in mod_front['bindings']: if bind['name'] not in lb_binds_list: - pass self._lb_bind_frontend( front, bind['name'], @@ -6991,17 +6402,14 @@ class DecortController(object): bind['port'], ) else: - lb_bind, = list(filter(lambda i: i['name'] == bind['name'],lb_front['bindings'])) - del lb_bind['guid'] + lb_bind, = list(filter(lambda i: i.name == bind['name'],lb_front.bindings)) + bind_addr = bind.get('address') or bind_ip + if bind_addr != lb_bind.ip_addr or bind['port'] != lb_bind.port: - if not bind.get('address'): - bind['address'] = bind_ip - - if dict(sorted(bind.items())) != dict(sorted(lb_bind.items())): self._lb_bind_frontend( front, bind['name'], - bind['address'], + bind_addr, bind['port'], update=True, ) @@ -7205,11 +6613,14 @@ class DecortController(object): return zone_info - def check_account_vm_features(self, vm_feature: VMFeature) -> bool: - return vm_feature.value in self.acc_info['computeFeatures'] + def check_account_vm_features( + self, + vm_feature: sdk_types.VMFeature, + ) -> bool: + return vm_feature in self.acc_info.vm_features - def check_rg_vm_features(self, vm_feature: VMFeature) -> bool: - return vm_feature.value in self.rg_info['computeFeatures'] + def check_rg_vm_features(self, vm_feature: sdk_types.VMFeature) -> bool: + return vm_feature in self.rg_info.vm_features @waypoint def extnet_get(self, id: int) -> dict: diff --git a/requirements.txt b/requirements.txt index 14aeab2..3ba3f1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ ansible==11.6.0 requests==2.32.3 -git+https://repository.basistech.ru/BASIS/dynamix-python-sdk.git@1.4.latest +git+https://repository.basistech.ru/BASIS/dynamix-python-sdk.git@1.5.latest