This commit is contained in:
2025-05-07 14:08:17 +03:00
parent f8c32d609b
commit 4113719334
36 changed files with 10638 additions and 191 deletions

View File

@@ -3,14 +3,26 @@ from datetime import datetime
from enum import Enum
import json
import re
from typing import Any, Callable, Iterable, Literal, Optional, Tuple
from functools import wraps
from typing import (
Any,
Callable,
Iterable,
Literal,
Optional,
ParamSpec,
TypeVar,
)
import time
import jwt
import requests
from ansible.module_utils.basic import AnsibleModule, env_fallback
P = ParamSpec('P')
R = TypeVar('R')
class DecortController(object):
"""DecortController is a utility class that holds target controller context and handles API requests formatting
based on the requested authentication type.
@@ -112,7 +124,7 @@ class DecortController(object):
IMAGE_TYPES = [
'cdrom',
'linux',
'other',
'unknown',
'virtual',
'windows',
]
@@ -165,6 +177,14 @@ class DecortController(object):
DPDK = 'DPDK'
class AuditsSortableField(Enum):
Call = 'Call'
Guid = 'Guid'
ResponseTime = 'Response Time'
StatusCode = 'Status Code'
Time = 'Time'
class MESSAGES:
@staticmethod
def ssl_error(url: None | str = None):
@@ -283,6 +303,13 @@ class DecortController(object):
return msg
@staticmethod
def default_value_used(param_name: str, default_value: Any) -> str:
return (
f'{param_name} parameter is not specified, '
f'default value "{default_value}" will be used.'
)
def __init__(self, arg_amodule: AnsibleModule):
"""
Instantiate DecortController() class at the beginning of any DECORT module run to have the following:
@@ -325,10 +352,6 @@ class DecortController(object):
# if self.workflow_callback != "":
# self.workflow_callback_present = True
# The following will be initialized to the name of the user in DECORT controller, who corresponds to
# the credentials supplied as authentication information parameters.
self.decort_username = ''
# self.run_phase may eventually be deprecated in favor of self.results['waypoints']
self.run_phase = "Run phase: Initializing DecortController instance."
@@ -392,40 +415,37 @@ class DecortController(object):
if self.authenticator == "jwt":
# validate supplied JWT on the DECORT controller
self.validate_jwt() # this call will abort the script if validation fails
jwt_decoded = jwt.decode(self.jwt, algorithms=["ES384"], options={"verify_signature": False})
self.decort_username = jwt_decoded['username'] + "@" + jwt_decoded['iss']
else:
# Oauth2 based authorization mode
# obtain JWT from Oauth2 provider and validate on the DECORT controller
self.obtain_jwt()
if self.controller_url is not None:
self.validate_jwt() # this call will abort the script if validation fails
jwt_decoded = jwt.decode(self.jwt, algorithms=["ES384"], options={"verify_signature": False})
self.decort_username = jwt_decoded['username'] + "@" + jwt_decoded['iss']
# self.run_phase = "Initializing DecortController instance complete."
return
@staticmethod
def waypoint(orig_f: Callable) -> Callable:
def waypoint(orig_f: Callable[P, R]) -> Callable[P, R]:
"""
A decorator for adding the name of called method to the string
`self.result['waypoints']`.
"""
@wraps(orig_f)
def new_f(self, *args, **kwargs):
self.result['waypoints'] += f' -> {orig_f.__name__}'
return orig_f(self, *args, **kwargs)
new_f.__name__ = orig_f.__name__
return new_f
@staticmethod
def checkmode(orig_f: Callable) -> Callable:
def checkmode(orig_f: Callable[P, R]) -> Callable[P, R | None]:
"""
A decorator for methods that should not executed in
Ansible Check Mode.
Instead of executing these methods, a message will be added
with the method name and the arguments with which it was called.
"""
@wraps(orig_f)
def new_f(self, *args, **kwargs):
if self.amodule.check_mode:
self.message(
@@ -437,7 +457,6 @@ class DecortController(object):
)
else:
return orig_f(self, *args, **kwargs)
new_f.__name__ = orig_f.__name__
return new_f
@staticmethod
@@ -978,6 +997,7 @@ class DecortController(object):
a['createdTime_readable'] = self.sec_to_dt_str(a['createdTime'])
a['deletedTime_readable'] = self.sec_to_dt_str(a['deletedTime'])
a['updatedTime_readable'] = self.sec_to_dt_str(a['updatedTime'])
a['description'] = a.pop('desc')
return accounts
@@ -1009,12 +1029,19 @@ class DecortController(object):
start_unix_time: None | int = None,
end_unix_time: None | int = None,
page_number: int = 1,
sort_by_asc: bool = True,
sort_by_field: None | AuditsSortableField = None,
) -> dict[str, Any]:
"""
Implementation of the functionality of API method
`/cloudapi/user/getAudit`.
"""
sort_by = None
if sort_by_field:
sort_by_prefix = '+' if sort_by_asc else '-'
sort_by = f'{sort_by_prefix}{sort_by_field.value}'
api_params = {
'call': api_method,
'minStatusCode': min_status_code,
@@ -1023,6 +1050,7 @@ class DecortController(object):
'timestampTo': end_unix_time,
'page': page_number,
'size': page_size,
'sortBy': sort_by,
}
api_resp = self.decort_api_call(
arg_req_function=requests.post,
@@ -1457,7 +1485,11 @@ class DecortController(object):
cpu_pin: bool = False,
hp_backed: bool = False,
numa_affinity: Literal['none', 'loose', 'strict'] = 'none',
preferred_cpu_cores: list[int] | None = None):
preferred_cpu_cores: list[int] | None = None,
boot_mode: Literal['bios', 'uefi'] = 'bios',
boot_loader_type: Literal['linux', 'windows', 'unknown'] = 'unknown',
network_interface_naming: Literal['eth', 'ens'] = 'ens',
hot_resize: bool = False,):
"""Manage KVM VM provisioning. To remove existing KVM VM compute instance use compute_remove method,
to resize use compute_resize, to manage power state use compute_powerstate method.
@@ -1501,6 +1533,10 @@ class DecortController(object):
if not image_id:
api_url = '/restmachine/cloudapi/kvmx86/createBlank'
api_params['bootType'] = boot_mode
api_params['loaderType'] = boot_loader_type
api_params['networkInterfaceNaming'] = network_interface_naming
api_params['hotResize'] = hot_resize
else:
api_url = '/restmachine/cloudapi/kvmx86/create'
api_params['imageId'] = image_id
@@ -1534,6 +1570,7 @@ class DecortController(object):
ifaces_for_delete = []
nets_for_attach = []
nets_for_change_ip = []
nets_for_change_mac_dict = {}
# Either only attaching or only detaching networks
if not ifaces or not new_networks:
@@ -1607,14 +1644,21 @@ class DecortController(object):
else:
nets_for_attach.append(net)
# Adding networks for change IP address
for net_key, net in unchangeable_nets_dict.items():
# Adding networks for change IP address
if net['type'] in ('VINS', 'EXTNET'):
old_ip = ifaces_dict[net_key]['ipAddress']
current_ip = ifaces_dict[net_key]['ipAddress']
new_ip = net['ip_addr']
if new_ip and old_ip != new_ip:
if new_ip and current_ip != new_ip:
nets_for_change_ip.append(net)
# Adding networks for change MAC address
if net['type'] != self.VMNetType.EMPTY.value:
current_mac = ifaces_dict[net_key]['mac']
new_mac = net['mac']
if new_mac and current_mac != new_mac:
nets_for_change_mac_dict[net_key] = net
# Detaching networks
for iface in ifaces_for_delete:
self.decort_api_call(
@@ -1639,6 +1683,7 @@ class DecortController(object):
'netId': net.get('id') or 0,
'ipAddr': net.get('ip_addr'),
'mtu': net.get('mtu'),
'mac_addr': net.get('mac'),
},
)
self.set_changed()
@@ -1649,10 +1694,23 @@ class DecortController(object):
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/changeIp',
arg_params={
'computeId': vm_id,
'netType': net['type'],
'netId': net['id'],
'ipAddr': net['ip_addr'],
'compute_id': vm_id,
'net_type': net['type'],
'net_id': net['id'],
'ip_addr': net['ip_addr'],
},
)
self.set_changed()
# Changing MAC adresses
for net_key, net in nets_for_change_mac_dict.items():
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/changeMac',
arg_params={
'compute_id': vm_id,
'current_mac_address': ifaces_dict[net_key]['mac'],
'new_mac_address': net['mac'],
},
)
self.set_changed()
@@ -1717,8 +1775,7 @@ class DecortController(object):
self.result['msg'] = "compute_restore() in check mode: restore Compute ID {} was requested.".format(comp_id)
return
api_params = dict(computeId=comp_id,
reason="Restored on user {} request by Ansible DECORT module.".format(self.decort_username))
api_params = dict(computeId=comp_id)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/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
@@ -1997,6 +2054,10 @@ class DecortController(object):
description: Optional[str] = None,
auto_start: Optional[bool] = None,
preferred_cpu_cores: list[int] | None = None,
boot_mode: None | Literal['bios', 'uefi'] = None,
boot_loader_type: None | Literal['linux', 'windows', 'unknown'] = None,
network_interface_naming: None | Literal['eth', 'ens'] = None,
hot_resize: None | bool = None,
):
OBJ = 'compute'
@@ -2015,6 +2076,10 @@ class DecortController(object):
'preferredCpu': (
[-1] if preferred_cpu_cores == [] else preferred_cpu_cores
),
'bootType': boot_mode,
'loaderType': boot_loader_type,
'networkInterfaceNaming': network_interface_naming,
'hotResize': hot_resize,
},
)
@@ -2029,6 +2094,10 @@ class DecortController(object):
'description': description,
'auto_start': auto_start,
'preferred_cpu_cores': preferred_cpu_cores,
'boot_mode': boot_mode,
'loader_type': boot_loader_type,
'network_interface_naming': network_interface_naming,
'hot_resize': hot_resize,
}
for param, value in params_to_check.items():
if value is not None:
@@ -2315,18 +2384,43 @@ class DecortController(object):
return 0, None
def image_create(self,img_name,url,gid,boottype,imagetype,drivers,hotresize,username,password,account_Id,usernameDL,passwordDL,sepId,poolName):
def image_create(
self,
img_name,
url,
gid,
drivers,
username,
password,
account_Id,
usernameDL,
passwordDL,
sepId,
poolName,
boot_mode: Literal['bios', 'uefi'] = 'bios',
boot_loader_type: Literal['linux', 'windows', 'unknown'] = 'unknown',
network_interface_naming: Literal['eth', 'ens'] = 'ens',
hot_resize: bool = False,
):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "image_create")
api_params = dict(name=img_name, url=url,
gid=gid, boottype=boottype,
imagetype=imagetype,
drivers=drivers, accountId=account_Id,
hotresize=hotresize, username=username,
password=password, usernameDL=usernameDL,
passwordDL=passwordDL, sepId=sepId,
poolName=poolName,
)
api_params = {
'name': img_name,
'url': url,
'gid': gid,
'boottype': boot_mode,
'imagetype': boot_loader_type,
'drivers': drivers,
'accountId': account_Id,
'hotresize': hot_resize,
'username': username,
'password': password,
'usernameDL': usernameDL,
'passwordDL': passwordDL,
'sepId': sepId,
'poolName': poolName,
'networkInterfaceNaming': network_interface_naming,
}
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/image/create", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json.
virt_image_dict = json.loads(api_resp.content.decode('utf8'))
@@ -2775,8 +2869,7 @@ class DecortController(object):
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,
reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username), )
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
@@ -2919,6 +3012,7 @@ class DecortController(object):
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']
@@ -3082,6 +3176,7 @@ class DecortController(object):
rg['createdTime_readable'] = self.sec_to_dt_str(rg['createdTime'])
rg['deletedTime_readable'] = self.sec_to_dt_str(rg['deletedTime'])
rg['updatedTime_readable'] = self.sec_to_dt_str(rg['updatedTime'])
rg['description'] = rg.pop('desc')
return resource_groups
@@ -3682,7 +3777,8 @@ class DecortController(object):
gpu_quota: None | int = None,
public_ip_quota: None | int = None,
ram_quota: None | int = None,
sep_pools: None | Iterable[str] = None,) -> None:
sep_pools: None | Iterable[str] = None,
description: None | str = None,) -> None:
"""
Implementation of functionality of the API method
`/cloudapi/account/update`.
@@ -3707,6 +3803,7 @@ class DecortController(object):
'name': name,
'sendAccessEmails': access_emails,
'uniqPools': sep_pools,
'desc': description,
},
not_fail_codes=[404]
)
@@ -4078,8 +4175,7 @@ class DecortController(object):
self.result['msg'] = "vins_restore() in check mode: restore ViNS ID {} was requested.".format(vins_id)
return
api_params = dict(vinsId=vins_id,
reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username), )
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
@@ -4121,8 +4217,7 @@ class DecortController(object):
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'],
reason='Changed by DECORT Ansible module, vins_state method.')
api_params = dict(vinsId=vins_dict['id'])
expected_state = ""
if vins_dict['status'] in ["CREATED", "ENABLED"] and desired_state == 'disabled':
@@ -4466,8 +4561,7 @@ class DecortController(object):
api_params = dict(diskId=disk_id,
detach=detach,
permanently=permanently,
reason=reason)
permanently=permanently)
self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/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
@@ -4594,7 +4688,6 @@ class DecortController(object):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_creation")
api_params = dict(accountId=accountId,
gid=0, # depricated
name=name,
description=description,
size=size,
@@ -4709,8 +4802,7 @@ class DecortController(object):
self.result['msg'] = "disk_restore() in check mode: restore Disk ID {} was requested.".format(disk_id)
return
api_params = dict(diskId=disk_id,
reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username), )
api_params = dict(diskId=disk_id)
self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/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
@@ -5757,7 +5849,13 @@ class DecortController(object):
self.result['msg'] = ("group_state(): no start/stop action required for B-service ID {} "
"to desired state '{}'.").format(bs_id,desired_state)
return
def group_resize_count(self,bs_id,gr_dict,desired_count):
def group_resize_count(
self,
bs_id,
gr_dict,
desired_count,
chipset: Literal['Q35', 'i440fx'] = 'i440fx',
):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_resize_count")
@@ -5767,7 +5865,8 @@ class DecortController(object):
serviceId=bs_id,
compgroupId=gr_dict['id'],
count=desired_count,
mode="ABSOLUTE"
mode="ABSOLUTE",
chipset=chipset,
)
api_url = "/restmachine/cloudapi/bservice/groupResize"
self.decort_api_call(requests.post, api_url, api_params)
@@ -5826,10 +5925,10 @@ class DecortController(object):
#rly need connect group to extnet ?
return
def group_provision(
self,bs_id,arg_name,arg_count=1,arg_cpu=1,arg_ram=1024,
self,bs_id,arg_name,chipset: Literal['Q35', 'i440fx'],arg_count=1,arg_cpu=1,arg_ram=1024,
arg_boot_disk=10,arg_image_id=0,arg_driver="KVM_X86",arg_role="",
arg_network=None,arg_timeout=0
):
arg_network=None,arg_timeout=0,
):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_provision")
@@ -5848,7 +5947,8 @@ class DecortController(object):
role = arg_role,
vinses = [n['id'] for n in arg_network if n['type'] == 'VINS'],
extnets = [n['id'] for n in arg_network if n['type'] == 'EXTNET'],
timeoutStart = arg_timeout
timeoutStart = arg_timeout,
chipset=chipset,
)
api_resp = self.decort_api_call(requests.post, api_url, api_params)
new_bsgroup_id = int(api_resp.text)