5.5.0
This commit is contained in:
@@ -29,13 +29,13 @@ Requirements:
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Callable
|
||||
from typing import Callable, Iterable
|
||||
import jwt
|
||||
import netaddr
|
||||
import time
|
||||
import requests
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
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
|
||||
@@ -161,7 +161,111 @@ class DecortController(object):
|
||||
VM_RESIZE_DOWN = 1
|
||||
VM_RESIZE_UP = 2
|
||||
|
||||
def __init__(self, arg_amodule):
|
||||
class MESSAGES:
|
||||
@staticmethod
|
||||
def obj_not_found(obj: str, id: None | int = None) -> str:
|
||||
with_id = f' with ID={id}' if id else ''
|
||||
return (
|
||||
f'Current user does not have access to the requested {obj}'
|
||||
f'{with_id} or non-existent {obj} specified.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def obj_deleted(obj: str, id: int, permanently=False, already=False):
|
||||
how_deleted = ' permanently' if permanently else ' to recycle bin'
|
||||
already_text = ' already' if already else ''
|
||||
return (
|
||||
f'The {obj} with ID={id} has{already_text} been'
|
||||
f' deleted{how_deleted}.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def obj_disabled(obj: str, id: int):
|
||||
return f'The {obj} with ID={id} has been disabled.'
|
||||
|
||||
@staticmethod
|
||||
def obj_enabled(obj: str, id: int):
|
||||
return f'The {obj} with ID={id} has been enabled.'
|
||||
|
||||
@staticmethod
|
||||
def obj_not_restored(obj: str, id: int):
|
||||
return (
|
||||
f'The {obj} with ID={id} cannot be restored'
|
||||
f' because it is not in recycle bin.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def obj_restored(obj: str, id: int):
|
||||
return f'The {obj} with ID={id} has been restored.'
|
||||
|
||||
@staticmethod
|
||||
def access_rights_granted(obj: str, id: int, user: str, rights: str):
|
||||
return (
|
||||
f'User "{user}" has been granted access rights "{rights}"'
|
||||
f' to the {obj} with ID={id}.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def access_rights_updated(obj: str, id: int, user: str, rights: str):
|
||||
return (
|
||||
f'Access rights to the {obj} with ID={id} for user "{user}"'
|
||||
f' has been updated to "{rights}".'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def access_rights_revoked(obj: str, id: int, user: str):
|
||||
return (
|
||||
f'Access rights to the {obj} with ID={id} for user "{user}"'
|
||||
f' has been revoked.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def obj_renamed(obj: str, id: int, new_name: str):
|
||||
return (
|
||||
f'The {obj} with ID={id} has been renamed to "{new_name}".'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def obj_smth_disabled(obj: str, id: int, smth: str):
|
||||
return (
|
||||
f'{smth[0].upper()}{smth[1:]} has been disabled for the {obj}'
|
||||
f' with ID={id}.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def obj_smth_enabled(obj: str, id: int, smth: str):
|
||||
return (
|
||||
f'{smth[0].upper()}{smth[1:]} has been enabled for the {obj}'
|
||||
f' with ID={id}.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def obj_smth_changed(obj: str, id: int, smth: str,
|
||||
new_value: str | int | float):
|
||||
if isinstance(new_value, str):
|
||||
value_str = f'"{new_value}"'
|
||||
else:
|
||||
value_str = new_value
|
||||
return (
|
||||
f'{smth[0].upper()}{smth[1:]} has been changed to {value_str}'
|
||||
f' for the {obj} with ID={id}.'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def method_in_check_mode(method_name: str, method_args: tuple,
|
||||
method_kwargs: dict) -> str:
|
||||
msg = (
|
||||
f'The method "{method_name}" was called in check mode'
|
||||
f' with arguments:'
|
||||
)
|
||||
for arg in method_args:
|
||||
msg += f'\n {arg}'
|
||||
for k, v in method_kwargs.items():
|
||||
msg += f'\n {k}={v}'
|
||||
|
||||
return msg
|
||||
|
||||
def __init__(self, arg_amodule: AnsibleModule):
|
||||
"""
|
||||
Instantiate DecortController() class at the beginning of any DECORT module run to have the following:
|
||||
- check authentication parameters to make sure all required parameters are properly specified
|
||||
@@ -175,6 +279,7 @@ class DecortController(object):
|
||||
"""
|
||||
|
||||
self.amodule = arg_amodule # AnsibleModule class instance
|
||||
self.aparams = self.amodule.params
|
||||
|
||||
# Note that the value for 'changed' key is by default set to 'False'. If you plan to manage value of 'changed'
|
||||
# key outside of DecortController() class, make sure you only update to 'True' when you really change the state
|
||||
@@ -278,6 +383,29 @@ class DecortController(object):
|
||||
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:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
def new_f(self, *args, **kwargs):
|
||||
if self.amodule.check_mode:
|
||||
self.message(
|
||||
self.MESSAGES.method_in_check_mode(
|
||||
method_name=orig_f.__name__,
|
||||
method_args=args,
|
||||
method_kwargs=kwargs,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return orig_f(self, *args, **kwargs)
|
||||
new_f.__name__ = orig_f.__name__
|
||||
return new_f
|
||||
|
||||
@staticmethod
|
||||
@@ -286,6 +414,102 @@ class DecortController(object):
|
||||
return time.strftime(str_format, time.gmtime(sec))
|
||||
return ''
|
||||
|
||||
@property
|
||||
def common_amodule_init_args(self) -> dict:
|
||||
return dict(
|
||||
argument_spec=dict(
|
||||
app_id=dict(
|
||||
type='str',
|
||||
fallback=(env_fallback, ['DECORT_APP_ID'])
|
||||
),
|
||||
app_secret=dict(
|
||||
type='str',
|
||||
fallback=(env_fallback, ['DECORT_APP_SECRET']),
|
||||
no_log=True
|
||||
),
|
||||
authenticator=dict(
|
||||
type='str',
|
||||
required=True,
|
||||
choices=['oauth2', 'jwt']
|
||||
),
|
||||
controller_url=dict(
|
||||
type='str',
|
||||
required=True
|
||||
),
|
||||
jwt=dict(
|
||||
type='str',
|
||||
fallback=(env_fallback, ['DECORT_JWT']),
|
||||
no_log=True
|
||||
),
|
||||
oauth2_url=dict(
|
||||
type='str',
|
||||
fallback=(env_fallback, ['DECORT_OAUTH2_URL'])
|
||||
),
|
||||
verify_ssl=dict(
|
||||
type='bool',
|
||||
default=True
|
||||
),
|
||||
),
|
||||
required_if=[
|
||||
('authenticator', 'oauth2',
|
||||
('oauth2_url', 'app_id', 'app_secret')),
|
||||
('authenticator', 'jwt', ('jwt',))
|
||||
],
|
||||
)
|
||||
|
||||
def set_changed(self):
|
||||
self.result['changed'] = True
|
||||
|
||||
def exit(self, fail=False):
|
||||
"""
|
||||
Append the dictionary `self.facts` to the dictionary
|
||||
`self.result` and call `self.amodule.exit_json(**self.result)`
|
||||
or `self.amodule.fail_json(**self.result)` if `fail=True`.
|
||||
"""
|
||||
|
||||
if getattr(self, 'facts', None):
|
||||
self.result['facts'] = getattr(self, 'facts')
|
||||
else:
|
||||
self.result['facts'] = dict()
|
||||
|
||||
if fail:
|
||||
self.amodule.fail_json(**self.result)
|
||||
else:
|
||||
self.amodule.exit_json(**self.result)
|
||||
|
||||
def message(self, msg: str):
|
||||
"""
|
||||
Append message to the new line of the string
|
||||
`self.result['msg']`.
|
||||
"""
|
||||
if self.result.get('msg'):
|
||||
self.result['msg'] += f'\n{msg}'
|
||||
else:
|
||||
self.result['msg'] = msg
|
||||
|
||||
def pack_amodule_init_args(self, **kwargs) -> dict:
|
||||
"""
|
||||
Pack arguments for creating AnsibleModule object.
|
||||
|
||||
@param (dict) kwargs: a dictionary with AnsibleModule
|
||||
constructor arguments to be merged with
|
||||
the dictionary `self.common_amodule_init_args`.
|
||||
"""
|
||||
|
||||
amodule_init_args = self.common_amodule_init_args
|
||||
|
||||
for arg_name in kwargs.keys():
|
||||
if amodule_init_args.get(arg_name):
|
||||
if isinstance(amodule_init_args[arg_name], dict):
|
||||
amodule_init_args[arg_name].update(kwargs[arg_name])
|
||||
continue
|
||||
if isinstance(amodule_init_args[arg_name], list):
|
||||
amodule_init_args[arg_name].extend(kwargs[arg_name])
|
||||
continue
|
||||
amodule_init_args[arg_name] = kwargs[arg_name]
|
||||
|
||||
return amodule_init_args
|
||||
|
||||
def check_amodule_argument(self, arg_name, abort=True):
|
||||
"""Checks if the argument identified by the arg_name is defined in the module parameters.
|
||||
|
||||
@@ -2072,6 +2296,7 @@ class DecortController(object):
|
||||
self.result['changed'] = True
|
||||
return
|
||||
|
||||
@waypoint
|
||||
def account_find(
|
||||
self,
|
||||
account_name: str = '',
|
||||
@@ -2150,8 +2375,6 @@ class DecortController(object):
|
||||
0 and empty dict otherwise (if `fail_if_not_found=False`).
|
||||
"""
|
||||
|
||||
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "account_find")
|
||||
|
||||
if not account_id and not account_name:
|
||||
self.result['msg'] = ('Cannot find account if account name and'
|
||||
' id are not specified.')
|
||||
@@ -2174,6 +2397,17 @@ class DecortController(object):
|
||||
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']
|
||||
for account in deleted_accounts_list:
|
||||
if account['name'] == account_name:
|
||||
_account_id = account['id']
|
||||
break
|
||||
|
||||
if _account_id:
|
||||
api_params = {
|
||||
@@ -2190,10 +2424,10 @@ class DecortController(object):
|
||||
|
||||
if not account_details:
|
||||
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)
|
||||
self.message(
|
||||
self.MESSAGES.obj_not_found(obj='account', id=_account_id)
|
||||
)
|
||||
self.exit(fail=True)
|
||||
return 0, None
|
||||
|
||||
account_details['computes_amount'] = account_details.pop('computes')
|
||||
@@ -2693,6 +2927,344 @@ class DecortController(object):
|
||||
|
||||
return audits
|
||||
|
||||
@waypoint
|
||||
@checkmode
|
||||
def account_delete(self, account_id: int, permanently=False) -> None:
|
||||
"""
|
||||
Implementation of functionality of the API method
|
||||
`/cloudapi/account/delete`.
|
||||
|
||||
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/delete',
|
||||
arg_params={
|
||||
'accountId': account_id,
|
||||
'permanently': permanently,
|
||||
},
|
||||
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_deleted(
|
||||
obj=OBJ,
|
||||
id=account_id,
|
||||
permanently=permanently)
|
||||
)
|
||||
self.set_changed()
|
||||
|
||||
@waypoint
|
||||
@checkmode
|
||||
def account_restore(self, account_id: int) -> None:
|
||||
"""
|
||||
Implementation of functionality of the API method
|
||||
`/cloudapi/account/restore`.
|
||||
|
||||
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/restore',
|
||||
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_restored(
|
||||
obj=OBJ,
|
||||
id=account_id,
|
||||
)
|
||||
)
|
||||
self.set_changed()
|
||||
|
||||
@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,
|
||||
add_users: None | dict[str, str] = None,
|
||||
del_users: None | Iterable[str] = None,
|
||||
upd_users: None | dict[str, str] = None) -> None:
|
||||
"""
|
||||
Implementation of the functionality of API methods
|
||||
`/cloudapi/account/addUser`, `/cloudapi/account/deleteUser`
|
||||
and `/cloudapi/account/updateUser`.
|
||||
|
||||
The method `self.exit(fail=True)` will be
|
||||
called if account or user is not found.
|
||||
|
||||
@param add_users: a dictionary where the key is user ID
|
||||
and the value is user access rights.
|
||||
|
||||
@param upd_users: a dictionary where the key is user ID
|
||||
and the value is new user access rights.
|
||||
|
||||
@param del_users: an Iterable object where the elements
|
||||
are user IDs.
|
||||
"""
|
||||
|
||||
OBJ = 'account'
|
||||
|
||||
for user_id, rights in add_users.items() if add_users else '':
|
||||
api_resp = self.decort_api_call(
|
||||
arg_req_function=requests.post,
|
||||
arg_api_name='/restmachine/cloudapi/account/addUser',
|
||||
arg_params={
|
||||
'accountId': account_id,
|
||||
'userId': user_id,
|
||||
'accesstype': rights,
|
||||
},
|
||||
not_fail_codes=[404]
|
||||
)
|
||||
|
||||
if api_resp.status_code == 404:
|
||||
self.message(
|
||||
f'User "{user_id}" or account with ID={account_id}'
|
||||
f' not found. API response text: {api_resp.text}'
|
||||
)
|
||||
self.exit(fail=True)
|
||||
|
||||
self.message(
|
||||
self.MESSAGES.access_rights_granted(
|
||||
obj=OBJ,
|
||||
id=account_id,
|
||||
user=user_id,
|
||||
rights=rights,
|
||||
)
|
||||
)
|
||||
|
||||
self.set_changed()
|
||||
|
||||
for user_id, rights in upd_users.items() if upd_users else '':
|
||||
api_resp = self.decort_api_call(
|
||||
arg_req_function=requests.post,
|
||||
arg_api_name='/restmachine/cloudapi/account/updateUser',
|
||||
arg_params={
|
||||
'accountId': account_id,
|
||||
'userId': user_id,
|
||||
'accesstype': rights,
|
||||
},
|
||||
not_fail_codes=[404]
|
||||
)
|
||||
|
||||
if api_resp.status_code == 404:
|
||||
self.message(
|
||||
f'User "{user_id}" or account with ID={account_id}'
|
||||
f' not found. API response text: {api_resp.text}'
|
||||
)
|
||||
self.exit(fail=True)
|
||||
|
||||
self.message(
|
||||
self.MESSAGES.access_rights_updated(
|
||||
obj=OBJ,
|
||||
id=account_id,
|
||||
user=user_id,
|
||||
rights=rights,
|
||||
)
|
||||
)
|
||||
|
||||
self.set_changed()
|
||||
|
||||
for user_id in del_users if del_users else '':
|
||||
api_resp = self.decort_api_call(
|
||||
arg_req_function=requests.post,
|
||||
arg_api_name='/restmachine/cloudapi/account/deleteUser',
|
||||
arg_params={
|
||||
'accountId': account_id,
|
||||
'userId': user_id,
|
||||
},
|
||||
not_fail_codes=[404]
|
||||
)
|
||||
|
||||
if api_resp.status_code == 404:
|
||||
self.message(
|
||||
f'User "{user_id}" not found.'
|
||||
f' API response text: {api_resp.text}'
|
||||
)
|
||||
self.exit(fail=True)
|
||||
|
||||
self.message(
|
||||
self.MESSAGES.access_rights_revoked(
|
||||
obj=OBJ,
|
||||
id=account_id,
|
||||
user=user_id,
|
||||
)
|
||||
)
|
||||
|
||||
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,
|
||||
ext_traffic_quota: None | int = None,
|
||||
gpu_quota: None | int = None,
|
||||
public_ip_quota: None | int = None,
|
||||
ram_quota: 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,
|
||||
'maxNetworkPeerTransfer': ext_traffic_quota,
|
||||
'maxNumPublicIP': public_ip_quota,
|
||||
'maxVDiskCapacity': disks_size_quota,
|
||||
'name': name,
|
||||
'sendAccessEmails': access_emails,
|
||||
},
|
||||
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,
|
||||
'external networks traffic quota': ext_traffic_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)
|
||||
)
|
||||
|
||||
self.set_changed()
|
||||
|
||||
###################################
|
||||
# GPU resource manipulation methods
|
||||
###################################
|
||||
|
||||
Reference in New Issue
Block a user