This commit is contained in:
2024-09-03 09:20:19 +03:00
parent ba305a0ccb
commit aa3f84095f
21 changed files with 8956 additions and 7 deletions

View File

@@ -28,15 +28,19 @@ Requirements:
- DECORT cloud platform version 3.8.6 or higher
"""
from datetime import datetime
from enum import Enum
import json
from typing import Callable, Iterable
import re
from typing import Any, Callable, Iterable
import time
import jwt
import netaddr
import time
import requests
from ansible.module_utils.basic import AnsibleModule, env_fallback
class DecortController(object):
"""DecortController is a utility class that holds target controller context and handles API requests formatting
based on the requested authentication type.
@@ -161,7 +165,38 @@ class DecortController(object):
VM_RESIZE_DOWN = 1
VM_RESIZE_UP = 2
class AccountStatus(Enum):
CONFIRMED = 'CONFIRMED'
DELETED = 'DELETED'
DESTROYED = 'DESTROYED'
DESTROYING = 'DESTROYING'
DISABLED = 'DISABLED'
class AccountSortableField(Enum):
createdTime = 'createdTime'
deletedTime = 'deletedTime'
id = 'id'
name = 'name'
status = 'status'
updatedTime = 'updatedTime'
class AccountUserRights(Enum):
R = 'R'
RCX = 'RCX'
ARCXDU = 'ARCXDU'
CXDRAU = 'CXDRAU'
class MESSAGES:
@staticmethod
def ssl_error(url: None | str = None):
url_text = f' while connecting to {url}' if url else ''
return (
f'An SSL error occurred{url_text}.'
f' If your platform is using a self-signed'
f' SSL certificate, see description for'
f' parameter `verify_ssl` in the modules docs.'
)
@staticmethod
def obj_not_found(obj: str, id: None | int = None) -> str:
with_id = f' with ID={id}' if id else ''
@@ -251,6 +286,10 @@ class DecortController(object):
f' for the {obj} with ID={id}.'
)
@staticmethod
def str_not_parsed(string: str):
return f'The string "{string}" cannot be parsed.'
@staticmethod
def method_in_check_mode(method_name: str, method_args: tuple,
method_kwargs: dict) -> str:
@@ -408,8 +447,36 @@ class DecortController(object):
new_f.__name__ = orig_f.__name__
return new_f
@staticmethod
def dt_str_to_sec(dt_str) -> None | int:
"""
Convert the datetime string to the Unix-time int.
The format of `dt_str`: `yyyymmddhhmmss` with the ability
to use any delimiter between digit groups (for example:
`yyyy-mm-dd hh:mm:ss`).
"""
re_pattern = re.compile(
r'^(?P<year>\d{4}).?(?P<month>\d{2})'
r'.?(?P<day>\d{2}).?(?P<hour>\d{2})'
r'.?(?P<minute>\d{2}).?(?P<second>\d{2})$'
)
re_match = re_pattern.match(dt_str)
if not re_match:
return
datetime_args = {k: int(v) for k, v in re_match.groupdict().items()}
return int(datetime(**datetime_args).timestamp())
@staticmethod
def sec_to_dt_str(sec: int, str_format: str = '%Y-%m-%d_%H-%M-%S') -> str:
"""
Convert the Unix-time int to the datetime string of the format
from `str_format`.
"""
if sec:
return time.strftime(str_format, time.gmtime(sec))
return ''
@@ -556,6 +623,9 @@ class DecortController(object):
# catch requests.exceptions.ConnectionError to handle incorrect oauth2_url case
try:
token_get_resp = requests.post(token_get_url, data=req_data, verify=self.verify_ssl)
except requests.exceptions.SSLError:
self.message(self.MESSAGES.ssl_error(url=token_get_url))
self.exit(fail=True)
except requests.exceptions.ConnectionError as errco:
self.result['failed'] = True
self.result['msg'] = "Failed to connect to '{}' to obtain JWT access token: {}".format(token_get_url, errco)
@@ -620,6 +690,9 @@ class DecortController(object):
try:
api_resp = requests.post(req_url, headers=req_header, verify=self.verify_ssl)
except requests.exceptions.SSLError:
self.message(self.MESSAGES.ssl_error(url=req_url))
self.exit(fail=True)
except requests.exceptions.ConnectionError:
self.result['failed'] = True
self.result['msg'] = "Failed to connect to '{}' while validating JWT".format(req_url)
@@ -665,6 +738,9 @@ class DecortController(object):
try:
api_resp = requests.post(req_url, data=req_data, verify=self.verify_ssl)
except requests.exceptions.SSLError:
self.message(self.MESSAGES.ssl_error(url=req_url))
self.exit(fail=True)
except requests.exceptions.ConnectionError:
self.result['failed'] = True
self.result['msg'] = "Failed to connect to '{}' while validating legacy user".format(req_url)
@@ -746,6 +822,9 @@ class DecortController(object):
params=arg_params,
headers=http_headers,
verify=self.verify_ssl)
except requests.exceptions.SSLError:
self.message(self.MESSAGES.ssl_error(url=req_url))
self.exit(fail=True)
except requests.exceptions.ConnectionError:
self.result['failed'] = True
self.result['msg'] = "Failed to connect to '{}' when calling DECORT API.".format(api_resp.url)
@@ -784,6 +863,193 @@ class DecortController(object):
self.amodule.fail_json(**self.result)
return None
@waypoint
def user_whoami(self) -> dict:
"""
Implementation of functionality of the API method
`/system/usermanager/whoami`.
"""
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/system/usermanager/whoami',
arg_params={},
)
return api_resp.json()
@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_accounts(
self,
account_id: None | int = None,
account_name: None | str = None,
account_status: None | AccountStatus = None,
account_user_rights: None | AccountUserRights = None,
deleted: bool = False,
page_number: int = 1,
page_size: None | int = None,
resource_consumption: bool = False,
sort_by_asc=True,
sort_by_field: None | AccountSortableField = None,
) -> list[dict]:
"""
Implementation of the functionality of API methods
`/cloudapi/account/list`,
`/cloudapi/account/listDeleted` and
`/cloudapi/account/listResourceConsumption`.
"""
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 = {
'by_id': account_id,
'name': account_name,
'acl': account_user_rights.value if account_user_rights else None,
'page': page_number if page_size else None,
'size': page_size,
'sortBy': sort_by,
'status': account_status.value if account_status else None,
}
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name={
False: '/restmachine/cloudapi/account/list',
True: '/restmachine/cloudapi/account/listDeleted',
}[deleted],
arg_params=api_params,
)
accounts = api_resp.json()['data']
if resource_consumption and not deleted:
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name=(
'/restmachine/cloudapi/account/listResourceConsumption'
),
arg_params={},
)
accounts_rc_list = api_resp.json()['data']
accounts_rc_dict = {a['id']: a for a in accounts_rc_list}
for a in accounts:
a_id = a['id']
a['resource_consumed'] = accounts_rc_dict[a_id]['Consumed']
a['resource_reserved'] = accounts_rc_dict[a_id]['Reserved']
for a in accounts:
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'])
return accounts
@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_audits(self,
api_method: None | str = None,
http_status_code: None | int = None,
start_unix_time: None | int = None,
end_unix_time: None | int = None,
page_number: int = 1,
page_size: None | int = None) -> dict[str, Any]:
"""
Implementation of the functionality of API method
`/cloudapi/user/getAudit`.
"""
api_params = {
'call': api_method,
'statuscode': http_status_code,
'timestampAt': start_unix_time,
'timestampTo': end_unix_time,
'page': page_number if page_size else None,
'size': page_size,
}
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/user/getAudit',
arg_params=api_params,
)
audits = api_resp.json()['data']
for a in audits:
a['Time_readable'] = self.sec_to_dt_str(a['Time'])
return audits
@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]:
"""
Implementation of the functionality of API method
`/cloudapi/user/search`.
"""
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/user/search',
arg_params={
'text': search_string,
},
)
return api_resp.json()
###################################
# Compute and KVM VM resource manipulation methods
###################################