You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
decort-ansible/module_utils/decort_utils.py

8543 lines
343 KiB

1 year ago
from copy import deepcopy
1 year ago
from datetime import datetime
from enum import Enum
import json
1 year ago
import re
7 months ago
from functools import wraps
from typing import (
Any,
Callable,
Iterable,
Literal,
Optional,
ParamSpec,
TypeVar,
)
1 year ago
import time
import requests
1 year ago
from ansible.module_utils.basic import AnsibleModule, env_fallback
5 months ago
import urllib3
1 year ago
7 months ago
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.
"""
5 months ago
acc_id: None | int = None
_acc_info: None | dict = None
rg_id: None | int = None
_rg_info: None | dict = None
FIELDS_FOR_SORTING_ACCOUNT_COMPUTE_LIST = [
'cpus',
'createdBy',
'createdTime',
'deletedBy',
'deletedTime',
'id',
'name',
'ram',
'registered',
'rgId',
'rgName',
'status',
'techStatus',
'totalDisksSize',
'updatedBy',
'updatedTime',
'userManaged',
'vinsConnected',
]
FIELDS_FOR_SORTING_ACCOUNT_DISK_LIST = [
'id',
'name',
'pool',
'sepId',
'shareable',
'sizeMax',
'type',
]
FIELDS_FOR_SORTING_ACCOUNT_IMAGE_LIST = [
'UNCPath',
'desc',
'id',
'name',
'public',
'size',
'status',
'type',
'username',
]
FIELDS_FOR_SORTING_ACCOUNT_RG_LIST = [
'createdBy',
'createdTime',
'deletedBy',
'deletedTime',
'id',
'milestones',
'name',
'status',
'updatedBy',
'updatedTime',
'vinses',
]
FIELDS_FOR_SORTING_ACCOUNT_VINS_LIST = [
'computes',
'createdBy',
'createdTime',
'deletedBy',
'deletedTime',
'externalIP',
'extnetId',
'freeIPs',
'id',
'name',
'network',
'priVnfDevId',
'rgId',
'rgName',
'status',
'updatedBy',
'updatedTime',
]
COMPUTE_TECH_STATUSES = [
'BACKUP_RUNNING',
'BACKUP_STOPPED',
5 months ago
'CLONING',
'DOWN',
5 months ago
'MERGE',
'MIGRATING',
3 weeks ago
'MIGRATING_IN',
'MIGRATING_OUT',
'PAUSED',
'PAUSING',
5 months ago
'ROLLBACK',
'SCHEDULED',
5 months ago
'SNAPCREATE',
'STARTED',
'STARTING',
'STOPPED',
'STOPPING',
]
DISK_TYPES = ['B', 'D']
IMAGE_TYPES = [
'cdrom',
'linux',
7 months ago
'unknown',
'virtual',
'windows',
]
RESOURCE_GROUP_STATUSES = [
'CREATED',
'DELETED',
'DELETING',
'DESTROYED',
'DESTROYING',
'DISABLED',
'DISABLING',
'ENABLED',
'ENABLING',
'MODELED',
'RESTORING',
]
VM_RESIZE_NOT = 0
VM_RESIZE_DOWN = 1
VM_RESIZE_UP = 2
1 year ago
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'
12 months ago
class VMNetType(Enum):
VINS = 'VINS'
EXTNET = 'EXTNET'
VFNIC = 'VFNIC'
EMPTY = 'EMPTY'
DPDK = 'DPDK'
5 months ago
TRUNK = 'TRUNK'
SDN = 'SDN'
12 months ago
7 months ago
class AuditsSortableField(Enum):
Call = 'Call'
Guid = 'Guid'
ResponseTime = 'Response Time'
StatusCode = 'Status Code'
Time = 'Time'
5 months ago
class ZoneField(Enum):
created_timestamp = 'createdTime'
deletable = 'deletable'
description = 'description'
grid_id = 'gid'
guid = 'guid'
id = 'id'
name = 'name'
node_ids = 'nodeIds'
status = 'status'
updated_timestamp = 'updatedTime'
class ZoneStatus(Enum):
CREATED = 'CREATED'
DESTROYED = 'DESTROYED'
class TrunkStatus(Enum):
CREATED = 'CREATED'
DESTROYED = 'DESTROYED'
DESTROYING = 'DESTROYING'
DISABLED = 'DISABLED'
ENABLED = 'ENABLED'
ENABLING = 'ENABLING'
MODELED = 'MODELED'
class TrunksSortableField(Enum):
accountIds = 'account_ids'
created_at = 'created_timestamp'
created_by = 'created_by'
deleted_at = 'deleted_timestamp'
deleted_by = 'deleted_by'
description = 'description'
guid = 'guid'
id = 'id'
mac = 'mac'
name = 'name'
nativeVlanId = 'native_vlan_id'
ovsBridge = 'ovs_bridge'
status = 'status'
trunkTags = 'vlan_ids'
updated_at = 'updated_timestamp'
updated_by = 'updated_by'
TRUNK_VLAN_ID_MIN_VALUE = 1
TRUNK_VLAN_ID_MAX_VALUE = 4095
class VMFeature(Enum):
hugepages = 'hugepages'
numa = 'numa'
cpupin = 'cpupin'
vfnic = 'vfnic'
dpdk = 'dpdk'
changemac = 'changemac'
trunk = 'trunk'
3 weeks ago
class VMBootDevice(Enum):
hd = 'hd'
network = 'network'
cdrom = 'cdrom'
class StoragePolicyStatus(Enum):
DISABLED = 'DISABLED'
ENABLED = 'ENABLED'
class StoragePoliciesSortableField(Enum):
description = 'description'
guid = 'guid'
id = 'id'
limit_iops = 'iops_limit'
name = 'name'
access_seps_pools = 'sep_pools'
status = 'status'
usage = 'usage'
class SecurityGroupState(Enum):
absent = 'absent'
present = 'present'
class SecurityGroupRuleMode(Enum):
delete = 'delete'
match = 'match'
update = 'update'
class SecurityGroupRuleDirection(Enum):
INBOUND = 'inbound'
OUTBOUND = 'outbound'
class SecurityGroupRuleEtherType(Enum):
IPV4 = 'IPv4'
IPV6 = 'IPv6'
class SecurityGroupRuleProtocol(Enum):
ICMP = 'icmp'
TCP = 'tcp'
UDP = 'udp'
class SecurityGroupSortableField(Enum):
account_id = 'account_id'
description = 'description'
id = 'id'
name = 'name'
rules = 'rules'
created_timestamp = 'created_at'
created_by = 'created_by'
updated_timestamp = 'updated_at'
updated_by = 'updated_by'
1 year ago
class MESSAGES:
1 year ago
@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.'
)
1 year ago
@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}.'
)
1 year ago
@staticmethod
def str_not_parsed(string: str):
return f'The string "{string}" cannot be parsed.'
1 year ago
@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
7 months ago
@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.'
)
1 year ago
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
- initiate test connection to the specified DECORT controller and validates supplied credentias
- store validated authentication information for later use by DECORT API calls
- store AnsibleModule class instance to keep handy reference to the module context
If any of the required parameters are missing or supplied authentication information is invalid, an error
message will be generated and execution aborted in a way, that lets Ansible to pick this information up and
relay it upstream.
"""
self.amodule = arg_amodule # AnsibleModule class instance
5 months ago
self.aparams: dict[str, Any] = self.amodule.params
if self.aparams['verify_ssl'] is False:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# 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
# of the object being managed.
# The rare cases to reset it to False again will usually involve either module running in "check mode" or
# when you detect and error and are about to call exit_json() or fail_json()
self.result = {'failed': False, 'changed': False, 'waypoints': "Init"}
self.authenticator = arg_amodule.params['authenticator']
12 months ago
self.controller_url = arg_amodule.params.get('controller_url')
12 months ago
self.jwt = arg_amodule.params.get('jwt')
self.app_id = arg_amodule.params['app_id']
self.app_secret = arg_amodule.params['app_secret']
self.oauth2_url = arg_amodule.params['oauth2_url']
12 months ago
self.domain = arg_amodule.params['domain']
self.password = arg_amodule.params['password']
self.username = arg_amodule.params['username']
self.session_key = ''
# self.iconf = arg_iconf
self.verify_ssl = arg_amodule.params['verify_ssl']
self.workflow_callback_present = False
# self.workflow_callback = arg_amodule.params['workflow_callback']
# self.workflow_context = arg_amodule.params['workflow_context']
# if self.workflow_callback != "":
# self.workflow_callback_present = True
# self.run_phase may eventually be deprecated in favor of self.results['waypoints']
self.run_phase = "Run phase: Initializing DecortController instance."
if self.authenticator == "jwt":
if not self.jwt:
self.result['failed'] = True
self.result['msg'] = ("JWT based authentication requested, but no JWT specified. "
"Use 'jwt' parameter or set 'DECORT_JWT' environment variable")
self.amodule.fail_json(**self.result)
12 months ago
elif self.authenticator in ('oauth2', 'decs3o', 'bvs'):
if self.authenticator == 'oauth2':
self.result['warning'] = (
'"oauth2" authenticator type is deprecated and might be '
'removed in newer versions. Please use "decs3o" instead.'
)
if not self.app_id:
self.result['failed'] = True
self.result['msg'] = ("Oauth2 based authentication requested, but no application ID specified. "
"Use 'app_id' parameter or set 'DECORT_APP_ID' environment variable.")
self.amodule.fail_json(**self.result)
if not self.app_secret:
self.result['failed'] = True
self.result['msg'] = ("Oauth2 based authentication requested, but no application secret specified. "
"Use 'app_secret' parameter or set 'DECORT_APP_SECRET' environment variable.")
self.amodule.fail_json(**self.result)
if not self.oauth2_url:
self.result['failed'] = True
self.result['msg'] = ("Oauth2 base authentication requested, but no Oauth2 provider URL specified. "
"Use 'oauth2_url' parameter or set 'DECORT_OAUTH2_URL' environment variable.")
self.amodule.fail_json(**self.result)
12 months ago
if self.authenticator == 'bvs':
if not self.domain:
self.message(
'BVS authentication requested, but no domain '
'specified. Use "domain" parameter or set '
'"DECORT_DOMAIN" environment variable.'
)
self.exit(fail=True)
if not self.password:
self.message(
'BVS authentication requested, but no password '
'specified. Use "password" parameter or set '
'"DECORT_PASSWORD" environment variable.'
)
self.exit(fail=True)
if not self.username:
self.message(
'BVS authentication requested, but no user specified. '
'Use "user" parameter or set "DECORT_USER" '
'environment variable.'
)
self.exit(fail=True)
else:
# Unknown authenticator type specified - notify and exit
self.result['failed'] = True
self.result['msg'] = "Error: unknown authentication type '{}' requested.".format(self.authenticator)
self.amodule.fail_json(**self.result)
self.run_phase = "Run phase: Authenticating to DECORT controller."
if self.authenticator == "jwt":
# validate supplied JWT on the DECORT controller
self.validate_jwt() # this call will abort the script if validation fails
else:
12 months ago
# Oauth2 based authorization mode
# obtain JWT from Oauth2 provider and validate on the DECORT controller
12 months ago
self.obtain_jwt()
if self.controller_url is not None:
self.validate_jwt() # this call will abort the script if validation fails
# self.run_phase = "Initializing DecortController instance complete."
return
5 months ago
@property
def acc_info(self) -> dict:
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):
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']]
@property
def rg_info(self) -> dict:
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):
raise TypeError
self._rg_info = rg_info
return self._rg_info
@staticmethod
7 months ago
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']`.
"""
7 months ago
@wraps(orig_f)
def new_f(self, *args, **kwargs):
self.result['waypoints'] += f' -> {orig_f.__name__}'
return orig_f(self, *args, **kwargs)
1 year ago
return new_f
@staticmethod
7 months ago
def checkmode(orig_f: Callable[P, R]) -> Callable[P, R | None]:
1 year ago
"""
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.
"""
7 months ago
@wraps(orig_f)
1 year ago
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)
return new_f
1 year ago
@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
12 months ago
def sec_to_dt_str(sec: float, str_format: str = '%Y-%m-%d_%H-%M-%S') -> str:
1 year ago
"""
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 ''
1 year ago
@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',
12 months ago
choices=['oauth2', 'jwt', 'bvs', 'decs3o'],
default='decs3o',
1 year ago
),
controller_url=dict(
type='str',
required=True
),
12 months ago
domain=dict(
type='str',
fallback=(env_fallback, ['DECORT_DOMAIN']),
),
1 year ago
jwt=dict(
type='str',
fallback=(env_fallback, ['DECORT_JWT']),
no_log=True
),
oauth2_url=dict(
type='str',
fallback=(env_fallback, ['DECORT_OAUTH2_URL'])
),
12 months ago
password=dict(
type='str',
fallback=(env_fallback, ['DECORT_PASSWORD']),
),
username=dict(
type='str',
fallback=(env_fallback, ['DECORT_USERNAME']),
),
1 year ago
verify_ssl=dict(
type='bool',
default=True
),
),
required_if=[
12 months ago
(
'authenticator', 'oauth2',
('oauth2_url', 'app_id', 'app_secret'),
),
(
'authenticator', 'decs3o',
('oauth2_url', 'app_id', 'app_secret'),
),
(
'authenticator', 'bvs',
(
'oauth2_url', 'app_id', 'app_secret',
'domain', 'username', 'password',
),
),
1 year ago
('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)
10 months ago
def message(self, msg: str | None = None, warning: bool = False):
1 year ago
"""
Append message to the new line of the string
`self.result['msg']`.
"""
10 months ago
key_name = 'warning' if warning else 'msg'
if self.result.get(key_name):
self.result[key_name] += f'\n{msg}'
1 year ago
else:
10 months ago
self.result[key_name] = msg
1 year ago
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.
@param arg_name: string that defines the name of the module parameter (aka argument) to check.
@param abort: boolean flag that tells if module should abort its execution on failure to locate the
specified argument.
@return: True if argument is found, False otherwise (in abort=False mode).
"""
# Note that if certain module argument is defined in the dictionary, corresponding key will
# still be found in the module.params. However, if no default value for the argument is
# declared in the dictionary, it will be set to None upon module initialization.
if arg_name not in self.amodule.params or self.amodule.params[arg_name] == None:
if abort:
self.result['failed'] = True
self.result['msg'] = "Missing conditionally required argument: {}".format(arg_name)
self.amodule.fail_json(**self.result)
else:
return False
else:
return True
12 months ago
def obtain_jwt(self):
if self.authenticator in ('oauth2', 'decs3o'):
self.jwt = self.obtain_decs3o_jwt()
elif self.authenticator == 'bvs':
self.jwt = self.obtain_bvs_jwt()
def obtain_decs3o_jwt(self):
"""Obtain JWT from the Oauth2 DECS3O provider using application ID and application secret provided , as specified at
class instance init method.
If method fails to obtain JWT it will abort the execution of the script by calling AnsibleModule.fail_json()
method.
@return: JWT as string.
"""
token_get_url = self.oauth2_url + "/v1/oauth/access_token"
req_data = dict(grant_type="client_credentials",
client_id=self.app_id,
client_secret=self.app_secret,
response_type="id_token",
4 years ago
validity=3600, )
# TODO: Need standard code snippet to handle server timeouts gracefully
# Consider a few retries before giving up or use requests.Session & requests.HTTPAdapter
# see https://stackoverflow.com/questions/15431044/can-i-set-max-retries-for-requests-request
# 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)
1 year ago
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)
self.amodule.fail_json(**self.result)
except requests.exceptions.Timeout as errti:
self.result['failed'] = True
self.result['msg'] = "Timeout when trying to connect to '{}' to obtain JWT access token: {}".format(
token_get_url, errti)
self.amodule.fail_json(**self.result)
# alternative -- if resp == requests.codes.ok
if token_get_resp.status_code != 200:
self.result['failed'] = True
self.result['msg'] = ("Failed to obtain JWT access token from oauth2_url '{}' for app_id '{}': "
"HTTP status code {}, reason '{}'").format(token_get_url,
self.amodule.params['app_id'],
token_get_resp.status_code,
token_get_resp.reason)
self.amodule.fail_json(**self.result)
# Common return values: https://docs.ansible.com/ansible/2.3/common_return_values.html
self.jwt = token_get_resp.content.decode('utf8')
return self.jwt
12 months ago
def obtain_bvs_jwt(self):
"""
Obtain JWT from the Oauth2 BVS provider using
application ID, application secret, username and password provided
If method fails to obtain JWT it will abort the execution of the script
by calling self.exit(fail=True).
@return: JWT as string.
"""
token_get_url = (
f'{self.oauth2_url}/realms/{self.domain}/'
f'protocol/openid-connect/token'
)
request_data = {
'grant_type': 'password',
'client_id': self.app_id,
'client_secret': self.app_secret,
'username': self.username,
'password': self.password,
'response_type': 'token',
'scope': 'openid',
}
token_get_resp = None
try:
token_get_resp = requests.post(
url=token_get_url,
data=request_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 con_error:
self.message(
f'Failed to connect to "{token_get_url}" '
f'to obtain JWT access token: {con_error}'
)
self.exit(fail=True)
except requests.exceptions.Timeout as timeout_error:
self.message(
f'Timeout when trying to connect to "{token_get_url}" '
f'to obtain JWT access token: {timeout_error}'
)
self.exit(fail=True)
if token_get_resp.status_code != 200:
self.message(
f'Failed to obtain JWT access token from '
f'oauth2_url "{token_get_url}" '
f'for app_id "{self.amodule.params["app_id"]}": '
f'HTTP status code {token_get_resp.status_code}, '
f'reason "{token_get_resp.reason}"'
)
self.exit(fail=True)
self.jwt = token_get_resp.json()['access_token']
return self.jwt
def validate_jwt(self, arg_jwt=None):
"""Validate JWT against DECORT controller. JWT can be supplied as argument to this method. If None supplied as
argument, JWT will be taken from class attribute. DECORT controller URL will always be taken from the class
attribute assigned at instantiation.
Validation is accomplished by attempting API call that lists accounts for the invoking user.
@param arg_jwt: the JWT to validate. If set to None, then JWT from the class instance will be validated.
@return: True if validation succeeds. If validation fails, method aborts the execution by calling
AnsibleModule.fail_json() method.
"""
if not arg_jwt:
# If no JWT is passed as argument to this method, we will validate JWT stored in the class
# instance (if any)
arg_jwt = self.jwt
if not arg_jwt:
# arg_jwt is still None - it mans self.jwt is also None, so generate error and abort the script
self.result['failed'] = True
self.result['msg'] = "Cannot validate empty JWT."
self.amodule.fail_json(**self.result)
# The above call to fail_json will abort the script, so the following return statement will
# never be executed
return False
req_url = self.controller_url + "/restmachine/cloudapi/account/list"
4 years ago
req_header = dict(Authorization="bearer {}".format(arg_jwt), )
try:
api_resp = requests.post(req_url, headers=req_header, verify=self.verify_ssl)
1 year ago
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)
self.amodule.fail_json(**self.result)
return False # actually, this directive will never be executed as fail_json exits the script
except requests.exceptions.Timeout:
self.result['failed'] = True
self.result['msg'] = "Timeout when trying to connect to '{}' while validating JWT".format(req_url)
self.amodule.fail_json(**self.result)
return False
if api_resp.status_code != 200:
self.result['failed'] = True
self.result['msg'] = ("Failed to validate JWT access token for DECORT controller URL '{}': "
"HTTP status code {}, reason '{}', header '{}'").format(api_resp.url,
api_resp.status_code,
api_resp.reason, req_header)
self.amodule.fail_json(**self.result)
return False
# If we fall through here, then everything went well.
return True
def decort_api_call(
self,
1 year ago
arg_req_function,
arg_api_name,
arg_params,
arg_files=None,
not_fail_codes: None | list = None,
5 months ago
accept_json_response: bool = False,
) -> requests.Response:
"""
Wrapper around DECORT API calls. It uses authorization mode and
credentials validated at the class instance creation
to properly format API call and send it to
the DECORT controller URL.
If connection errors are detected, it aborts execution of
the script and relay error messages to upstream Ansible process.
If HTTP 503 error is detected the method will retry with
increasing timeout, and if after max_retries there still is
HTTP 503 error, it will abort as above. If any other HTTP error
is detected, the method will abort immediately as above.
@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
passed to the API call
@param arg_req_function: function object to be called as
part of API, e.g. requests.post or requests.get
@return: api call response object as returned by
the REST functions from Python "requests" module
"""
max_retries = 5
retry_counter = max_retries
http_headers = dict()
api_resp = None
req_url = self.controller_url + arg_api_name
12 months ago
http_headers['Authorization'] = 'bearer {}'.format(self.jwt)
5 months ago
if accept_json_response:
http_headers['Accept'] = 'application/json'
while retry_counter > 0:
try:
api_resp = arg_req_function(
req_url,
files=arg_files,
params=arg_params,
headers=http_headers,
verify=self.verify_ssl)
1 year ago
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
1 year ago
self.result['msg'] = "Failed to connect to '{}' when calling DECORT API.".format(api_resp.url if api_resp else req_url)
self.amodule.fail_json(**self.result)
return None # actually, this directive will never be executed as fail_json aborts the script
except requests.exceptions.Timeout:
self.result['failed'] = True
1 year ago
self.result['msg'] = "Timeout when trying to connect to '{}' when calling DECORT API.".format(api_resp.url if api_resp else req_url)
self.amodule.fail_json(**self.result)
return None
if api_resp.status_code == 200:
return api_resp
elif not_fail_codes and api_resp.status_code in not_fail_codes:
return api_resp
elif api_resp.status_code == 503:
retry_timeout = 5 + 10 * (max_retries - retry_counter)
time.sleep(retry_timeout)
retry_counter = retry_counter - 1
else:
self.result['failed'] = True
2 years ago
self.result['msg'] = (
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}.'
)
self.amodule.fail_json(**self.result)
return None # actually, this directive will never be executed as fail_json aborts the script
# if we get through here, it means that we were getting HTTP 503 while retrying - generate error
self.result['failed'] = True
self.result['msg'] = "Error when calling DECORT API '{}', HTTP status code '{}', reason '{}'.". \
format(api_resp.url, api_resp.status_code, api_resp.reason)
self.amodule.fail_json(**self.result)
return None
1 year ago
@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,
3 weeks ago
zone_id: None | int = None,
1 year ago
) -> 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,
3 weeks ago
'zone_id': zone_id,
1 year ago
}
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'])
7 months ago
a['description'] = a.pop('desc')
3 weeks ago
a['zone_ids'] = a.pop('zoneIds')
1 year ago
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,
1 year ago
page_size: int,
1 year ago
api_method: None | str = None,
1 year ago
min_status_code: None | int = None,
max_status_code: None | int = None,
1 year ago
start_unix_time: None | int = None,
end_unix_time: None | int = None,
page_number: int = 1,
7 months ago
sort_by_asc: bool = True,
sort_by_field: None | AuditsSortableField = None,
1 year ago
) -> dict[str, Any]:
1 year ago
"""
Implementation of the functionality of API method
`/cloudapi/user/getAudit`.
"""
7 months ago
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}'
1 year ago
api_params = {
'call': api_method,
1 year ago
'minStatusCode': min_status_code,
'maxStatusCode': max_status_code,
1 year ago
'timestampAt': start_unix_time,
'timestampTo': end_unix_time,
1 year ago
'page': page_number,
1 year ago
'size': page_size,
7 months ago
'sortBy': sort_by,
1 year ago
}
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:
3 weeks ago
a['timestamp_readable'] = self.sec_to_dt_str(a['timestamp'])
1 year ago
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()
5 months ago
@waypoint
def user_zones(
self,
page_size: None | int = None,
deletable: None | bool = None,
description: None | str = None,
grid_id: None | int = None,
id: None | int = None,
name: None | str = None,
node_id: None | int = None,
status: None | ZoneStatus = None,
page_number: int = 1,
sort_by_asc: bool = True,
sort_by_field: None | ZoneField = None,
) -> list[dict]:
"""
Implementation of the functionality of API method
`/cloudapi/zone/list`.
"""
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': id,
'gid': grid_id,
'name': name,
'description': description,
'status': status.value if status else None,
'deletable': deletable,
'nodeId': node_id,
'page': page_number if page_size else None,
'size': page_size,
'sortBy': sort_by,
}
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/zone/list',
arg_params=api_params,
)
zones_orig: list[dict] = api_resp.json()['data']
zones_result: list[dict] = []
for zone_orig in zones_orig:
zone_result = {}
for field in self.ZoneField:
zone_result[field.name] = zone_orig[field.value]
zones_result.append(zone_result)
return zones_result
###################################
# Compute and KVM VM resource manipulation methods
###################################
def compute_bootdisk_size(self, comp_dict, new_size):
"""Manages size of the boot disk. Note that the size of the boot disk can only grow. This method will issue
a warning if you try to reduce the size of the boot disk.
@param (int) comp_dict: dictionary with Compute facts. It identifies the Compute for which boot disk
size change is requested.
@param (int) new_size: new size of boot disk in GB. If new size is the same as the current boot disk size,
the method will do nothing. If new size is smaller, an error will occur.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "compute_bootdisk_size")
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = ("compute_bootdisk_size() in check mode: change boot disk size for Compute "
"ID {} was requested.").format(comp_dict['id'])
return
# Values that are specified via Jinja templating engine (e.g. "{{ new_size }}") may come
# as strings. To make sure comparison of new values against current compute size is done
# correcly, we explicitly cast them to type int here.
new_size = int(new_size)
bdisk_size = 0
bdisk_id = 0
for disk in comp_dict['disks']:
if disk['type'] == 'B':
bdisk_size = disk['sizeMax']
bdisk_id = disk['id']
break
else:
self.result['failed'] = True
self.result['msg'] = ("compute_bootdisk_size(): cannot identify boot disk for Compute "
"ID {}.").format(comp_dict['id'])
return
if new_size == bdisk_size:
self.result['failed'] = False
self.result['warning'] = ("compute_bootdisk_size(): new size {} is the same as current for "
"Compute ID {}, nothing to do.").format(new_size, comp_dict['id'])
return
api_params = dict(diskId=bdisk_id,
size=new_size)
# NOTE: we are using API "resize2", as in this module we are managing
# disks attached to compute(s) (DSF ver.2 only)
self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/resize2", 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
for disk in comp_dict['disks']:
if disk['type'] == 'B':
disk['sizeMax'] = new_size
break
return
9 months ago
@waypoint
@checkmode
3 weeks ago
def compute_disks(self, comp_dict: dict, aparam_disks_dict: dict):
aparam_disks = aparam_disks_dict.get('objects', [])
aparam_disks_ids = set([disk['id'] for disk in aparam_disks])
mode = aparam_disks_dict['mode']
comp_disks_ids = {disk['id'] for disk in comp_dict['disks']}
disks_ids_to_attach = set()
disks_ids_to_detach = set()
disks_ids_to_delete = set()
9 months ago
match mode:
case 'update':
3 weeks ago
disks_ids_to_attach = aparam_disks_ids - comp_disks_ids
9 months ago
case 'detach':
3 weeks ago
disks_ids_to_detach = aparam_disks_ids & comp_disks_ids
9 months ago
case 'delete':
3 weeks ago
disks_ids_to_delete = aparam_disks_ids & comp_disks_ids
9 months ago
case 'match':
3 weeks ago
disks_ids_to_attach = aparam_disks_ids - comp_disks_ids
disks_ids_to_detach = comp_disks_ids - aparam_disks_ids
disks_to_attach = []
if disks_ids_to_attach:
disks_to_attach = [
d for d in aparam_disks if d['id'] in disks_ids_to_attach
]
disks_to_detach = []
if disks_ids_to_detach:
disks_to_detach = [
d for d in comp_dict['disks'] if d['id'] in disks_ids_to_detach
]
disks_to_delete = []
if disks_ids_to_delete:
disks_to_delete = [
d for d in comp_dict['disks'] if d['id'] in disks_ids_to_delete
]
unchanged_disks_ids = (
aparam_disks_ids - disks_ids_to_attach -
disks_ids_to_detach - disks_ids_to_delete
)
unchanged_disks = [
d for d in aparam_disks if d['id'] in unchanged_disks_ids
]
for aparam_disk in unchanged_disks:
for comp_disk in comp_dict['disks']:
if comp_disk['id'] == aparam_disk['id']:
if (
(
aparam_disk['pci_slot_num_hex'] is not None
and str(comp_disk['pci_slot'])
!= aparam_disk['pci_slot_num_hex']
) or (
aparam_disk['bus_num_hex'] is not None
and str(comp_disk['bus_number'])
!= aparam_disk['bus_num_hex']
)
):
disks_to_detach.append(aparam_disk)
disks_to_attach.append(aparam_disk)
9 months ago
3 weeks ago
for disk in disks_to_detach:
9 months ago
api_params = {
'computeId': comp_dict['id'],
3 weeks ago
'diskId': disk['id'],
9 months ago
}
self.decort_api_call(
arg_req_function=requests.post,
3 weeks ago
arg_api_name='/restmachine/cloudapi/compute/diskDetach',
9 months ago
arg_params=api_params,
)
self.set_changed()
3 weeks ago
for disk in disks_to_attach:
pci_slot_num = disk['pci_slot_num_hex']
if pci_slot_num is not None:
pci_slot_num = hex(int(pci_slot_num))
bus_num = disk['bus_num_hex']
if bus_num is not None:
bus_num = hex(int(bus_num))
9 months ago
api_params = {
'computeId': comp_dict['id'],
3 weeks ago
'diskId': disk['id'],
'diskType': 'D',
'pci_slot': pci_slot_num,
'bus_number': bus_num,
9 months ago
}
self.decort_api_call(
arg_req_function=requests.post,
3 weeks ago
arg_api_name='/restmachine/cloudapi/compute/diskAttach',
9 months ago
arg_params=api_params,
)
self.set_changed()
3 weeks ago
for disk in disks_to_delete:
9 months ago
api_params = {
'computeId': comp_dict['id'],
3 weeks ago
'diskId': disk['id'],
9 months ago
}
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/diskDel',
arg_params=api_params,
)
self.set_changed()
4 years ago
9 months ago
@waypoint
@checkmode
def compute_boot_disk(self, comp_id: int, boot_disk: int):
api_params = {
'computeId': comp_id,
'diskId': boot_disk,
}
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/bootDiskSet',
arg_params=api_params,
)
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
3 weeks ago
return comp_id
9 months ago
def _compute_get_by_id(
self,
comp_id,
need_custom_fields: bool = False,
need_console_url: bool = False,
5 months ago
need_snapshot_merge_status: bool = False,
9 months ago
):
"""Helper function that locates compute instance by ID and returns Compute facts.
@param (int) comp_id: ID of the Compute instance to find and return facts for.
@return: (int) Compute ID, dictionary of Compute facts and RG ID where this Compute is located. Note
that if it fails to find the Compute for the specified ID, it may return 0 for ID and None for the
dictionary. So it is suggested to check the return values accordingly.
"""
ret_comp_id = 0
ret_comp_dict = None
ret_rg_id = 0
4 years ago
api_params = dict(computeId=comp_id, )
3 weeks ago
api_resp = self.decort_api_call(
requests.post,
'/restmachine/cloudapi/compute/get',
api_params,
not_fail_codes=[404],
)
if api_resp.status_code == 200:
ret_comp_id = comp_id
ret_comp_dict = json.loads(api_resp.content.decode('utf8'))
1 year ago
# Sorting network interface list by PCI slot number
ret_comp_dict['interfaces'].sort(key=lambda k: k['pciSlot'])
ret_rg_id = ret_comp_dict['rgId']
12 months ago
12 months ago
if need_custom_fields:
if ret_comp_dict['status'] in ('DESTROYED', 'DELETED'):
custom_fields = None
else:
custom_fields = self.compute_get_custom_fields(
compute_id=ret_comp_id,
)
ret_comp_dict['custom_fields'] = custom_fields
9 months ago
if need_console_url and ret_comp_dict['techStatus'] == 'STARTED':
console_url = self.compute_get_console_url(
compute_id=ret_comp_id,
)
ret_comp_dict['console_url'] = console_url
5 months ago
if need_snapshot_merge_status:
snapshot_merge_status = (
self.compute_get_snapshot_merge_status(
vm_id=ret_comp_id,
)
)
ret_comp_dict['snapshot_merge_status'] = snapshot_merge_status
3 weeks ago
elif api_resp.status_code == 404:
self.message(f'Compute with ID {comp_id} not found.')
self.exit(fail=True)
else:
self.result['warning'] = ("compute_get_by_id(): failed to get Compute by ID {}. HTTP code {}, "
"response {}.").format(comp_id, api_resp.status_code, api_resp.reason)
return ret_comp_id, ret_comp_dict, ret_rg_id
5 months ago
def compute_find(
self,
comp_id: int = 0,
comp_name="",
rg_id=0,
check_state=True,
need_custom_fields: bool = False,
need_console_url: bool = False,
need_snapshot_merge_status: bool = False,
):
"""Tries to find Compute instance according to the specified parameters. On success returns non-zero
Compute ID and a dictionary with Compute details, or 0 for ID and None for the dictionary on failure.
@param (int) comp_id: ID of the Compute to locate. If non zero comp_id is passed, it is assumed that
this Compute exists (check_state flag is ignored). Also comp_name and rg_id are ignored, when searching
by Compute ID.
@param (string) comp_name: name of the Compute to locate. Locating Compute instance by name requires
that comp_id is set to 0 and non-zero rg_id is specified.
@param (int) rg_id: ID of the RG to locate Compute instance in. This parameter is used when locating
Compute by name and ignored otherwise. Note that this method does not validate RG ID.
@param (bool) check_state: check that VM in valid state if True. Note that this check is skpped if
non-zero comp_id is passed to the method.
@return: (int) ret_comp_id - ID of the Compute on success (if the Compute was found), 0 otherwise.
@return: (dict) ret_comp_dict - dictionary with Compute details on success as returned by
/cloudapi/compute/get, None otherwise.
@return: (int) ret_rg_id - ID of the RG, where this Compute instance was found.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "compute_find")
COMP_INVALID_STATES = ["DESTROYED", "DELETED", "ERROR", "DESTROYING"]
ret_comp_id = 0
ret_comp_dict = None
# validated_rg_id = 0
# validated_rg_facts = None
if comp_id:
# locate Compute instance by ID - if there is no Compute with such ID, the method will abort
# upstream Ansible module execution by calling fail_json(...)
# Note that in this mode check_state argument is ignored.
12 months ago
ret_comp_id, ret_comp_dict, ret_rg_id = (
self._compute_get_by_id(
comp_id=comp_id,
need_custom_fields=need_custom_fields,
9 months ago
need_console_url=need_console_url,
5 months ago
need_snapshot_merge_status=need_snapshot_merge_status,
12 months ago
)
)
if not ret_comp_id:
self.result['failed'] = True
self.result['msg'] = "compute_find(): cannot locate Compute with ID {}.".format(comp_id)
self.amodule.fail_json(**self.result)
# fail the module - exit
else:
# If no comp_id specified, then we have to locate Compute by combination of compute name and RG ID
# Therefore, RG ID cannot be zero and compute name cannot be empty.
if not rg_id and comp_name == "":
self.result['failed'] = True
4 years ago
self.result[
'msg'] = "compute_find(): cannot find Compute by name when either name is empty or RG ID is zero."
self.amodule.fail_json(**self.result)
# fail the module - exit
4 years ago
api_params = dict(includedeleted=True, )
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/list", api_params)
if api_resp.status_code == 200:
comp_list = json.loads(api_resp.content.decode('utf8'))
else:
self.result['failed'] = True
self.result['msg'] = ("compute_find(): failed to get list Computes. HTTP code {}, "
4 years ago
"response {}.").format(api_resp.status_code, api_resp.reason)
self.amodule.fail_json(**self.result)
# fail the module - exit
4 years ago
# if we have validated RG ID at this point, look up Compute by name in this RG
# rg.vms list contains IDs of compute instances registered with this RG until compute is
# destroyed. So we may see here computes in "active" and DELETED states.
for runner in comp_list['data']:
if runner['name'] == comp_name and runner['rgId'] == rg_id:
if not check_state or runner['status'] not in COMP_INVALID_STATES:
ret_comp_id = runner['id']
# we still need to get compute info from the model to make sure
# compute dictionary contains complete data in correct format
12 months ago
_, ret_comp_dict, _ = self._compute_get_by_id(
comp_id=ret_comp_id,
need_custom_fields=need_custom_fields,
9 months ago
need_console_url=need_console_url,
5 months ago
need_snapshot_merge_status=need_snapshot_merge_status, # noqa: E501
12 months ago
)
break
# NOTE: if there were no errors, but compute was not found, ret_comp_id=0 is returned
return ret_comp_id, ret_comp_dict, rg_id
def compute_powerstate(self, comp_facts, target_state, force_change=True):
"""Manage Compute power state transitions or its guest OS restarts.
@param (dict) comp_facts: dictionary with Compute instance facts, which power state change is
requested.
@param (string) target_state: desired power state of this Compute. Allowed values are:
'poweredon', 'poweredoff', 'paused', 'halted', 'restarted',
@param (bool) force_change: tells if it is allowed to force power state transition for certain
cases (e.g. for transition into 'stop' state).
NOTE: this method may return before the actual change of Compute's power state occurs.
"""
# @param wait_for_change: integer number that tells how many 5 seconds intervals to wait for the power state
# change before returning from this method.
INVALID_MODEL_STATES = ["MIGRATING", "DELETED", "DESTROYING", "DESTROYED", "ERROR", "REDEPLOYING"]
INVALID_TECH_STATES = ["STOPPING", "STARTING", "PAUSING"]
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "compute_powerstate")
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = ("compute_powerstate() in check mode. Power state change of Compute ID {} "
"to '{}' was requested.").format(comp_facts['id'], target_state)
return
if comp_facts['status'] in INVALID_MODEL_STATES:
self.result['failed'] = False
self.result['msg'] = ("compute_powerstate(): no power state change possible for Compute ID {} "
"in its current state '{}'.").format(comp_facts['id'], comp_facts['status'])
return
if comp_facts['techStatus'] in INVALID_TECH_STATES:
self.result['failed'] = False
self.result['msg'] = ("compute_powerstate(): no power state change possible for Compute ID {} "
"in its current techState '{}'.").format(comp_facts['id'], comp_facts['techStatus'])
return
powerstate_api = "" # this string will also be used as a flag to indicate that API call is necessary
api_params = dict(computeId=comp_facts['id'])
expected_state = ""
if comp_facts['techStatus'] == "STARTED":
if target_state == 'paused':
powerstate_api = "/restmachine/cloudapi/compute/pause"
expected_techState = "PAUSED"
elif target_state in ('poweredoff', 'halted', 'stopped'):
powerstate_api = "/restmachine/cloudapi/compute/stop"
api_params['force'] = force_change
expected_techState = "STOPPED"
elif target_state == 'restarted':
powerstate_api = "/restmachine/cloudapi/compute/reboot"
expected_techState = "STARTED"
elif comp_facts['techStatus'] == "PAUSED" and target_state in ('poweredon', 'restarted', 'started'):
powerstate_api = "/restmachine/cloudapi/compute/resume"
expected_techState = "STARTED"
elif comp_facts['techStatus'] == "STOPPED" and target_state in ('poweredon', 'restarted', 'started'):
powerstate_api = "/restmachine/cloudapi/compute/start"
3 weeks ago
if (
target_state == 'started'
and self.aparams['boot'] is not None
and self.aparams['boot']['from_cdrom'] is not None
):
api_params['altBootId'] = self.aparams['boot']['from_cdrom']
expected_techState = "STARTED"
if powerstate_api != "":
self.decort_api_call(requests.post, powerstate_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
comp_facts['techStatus'] = expected_techState
else:
self.result['failed'] = False
self.result['warning'] = ("compute_powerstate(): no power state change required for Compute ID {} from its "
"current state '{}' to desired state '{}'.").format(comp_facts['id'],
4 years ago
comp_facts['techStatus'],
target_state)
return
3 weeks ago
def kvmvm_provision(
self,
rg_id,
comp_name,
cpu,
ram,
boot_disk_size,
image_id,
chipset: Literal['Q35', 'i440fx'] = 'i440fx',
description="",
userdata=None,
sep_id=None,
pool_name=None,
start_on_create=True,
cpu_pin: bool = False,
hp_backed: bool = False,
numa_affinity: Literal['none', 'loose', 'strict'] = '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,
zone_id: None | int = None,
storage_policy_id: None | int = None,
os_version: None | str = None,
5 months ago
):
1 year ago
"""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.
@param (int) rg_id: ID of the RG where the VM will be provisioned.
@param (string) comp_name: that specifies the name of the VM.
@param (int) cpu: how many virtual CPUs to allocate.
@param (int) ram: volume of RAM in MB to allocate (i.e. pass 4096 to allocate 4GB RAM).
@param (int) boot_disk: boot disk size in GB.
@param (int) image_id: ID of the OS image to base this Compute on.
1 year ago
@param (string) description: optional text description for the VM.
@param (string) userdata: additional paramters to pass to cloud-init facility of the guest OS.
@param (bool) start_on_create: set to False if you want the VM to be provisioned in HALTED state.
@return (int) ret_kvmvm_id: ID of provisioned VM.
Note: when Ansible is run in check mode method will return 0.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "kvmvm_provision2")
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = ("kvmvm_provision() in check mode. Provision KVM VM '{}' in RG ID {} "
"was requested.").format(comp_name, rg_id)
return 0
1 year ago
api_params = {
'rgId': rg_id,
'name': comp_name,
'cpu': cpu,
'ram': ram,
9 months ago
'bootDisk': boot_disk_size,
1 year ago
'sepId': sep_id,
'pool': pool_name,
'interfaces': '[]', # we create VM without any network connections
'chipset': chipset,
9 months ago
'withoutBootDisk': not boot_disk_size,
10 months ago
'preferredCpu': preferred_cpu_cores,
5 months ago
'zoneId': zone_id,
3 weeks ago
'storage_policy_id': storage_policy_id,
'os_version': os_version,
1 year ago
}
1 year ago
if description:
api_params['desc'] = description
1 year ago
if not image_id:
api_url = '/restmachine/cloudapi/kvmx86/createBlank'
7 months ago
api_params['bootType'] = boot_mode
api_params['loaderType'] = boot_loader_type
api_params['networkInterfaceNaming'] = network_interface_naming
api_params['hotResize'] = hot_resize
1 year ago
else:
api_url = '/restmachine/cloudapi/kvmx86/create'
api_params['imageId'] = image_id
api_params['start'] = start_on_create
1 year ago
api_params['cpupin'] = cpu_pin
api_params['hpBacked'] = hp_backed
api_params['numaAffinity'] = numa_affinity
1 year ago
if userdata:
api_params['userdata'] = json.dumps(userdata) # we need to pass a string object as "userdata"
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.
self.result['failed'] = False
self.result['changed'] = True
ret_vm_id = int(api_resp.content)
return ret_vm_id
1 year ago
@waypoint
@checkmode
def compute_networks(
self,
comp_dict: dict[str, Any],
new_networks: list[dict[str, Any]],
order_changing: bool = False,
):
vm_id = comp_dict['id']
ifaces = comp_dict['interfaces']
ifaces_for_delete = []
nets_for_attach = []
5 months ago
ifaces_dict = {}
new_nets_dict = {}
1 year ago
nets_for_change_ip = []
7 months ago
nets_for_change_mac_dict = {}
5 months ago
nets_for_change_mtu_dict = {}
3 weeks ago
nets_for_change_sec_groups_dict = {}
nets_for_change_state_dict = {}
1 year ago
# Either only attaching or only detaching networks
if not ifaces or not new_networks:
ifaces_for_delete = ifaces
nets_for_attach = new_networks
else:
# Creating dictionaries in which the key is a net type + a net id
12 months ago
# + (optional) postfix
1 year ago
# For empty networks a net id is the number of the empty network
empty_ifaces_count = 0
for iface in ifaces:
net_type = iface['netType']
12 months ago
if net_type == self.VMNetType.EMPTY.value:
1 year ago
empty_ifaces_count += 1
net_id = empty_ifaces_count
5 months ago
elif net_type == self.VMNetType.SDN.value:
net_id = iface['sdn_interface_id']
1 year ago
else:
net_id = iface['netId']
net_key = f'{net_type}{net_id}'
ifaces_dict[net_key] = iface
5 months ago
1 year ago
empty_new_nets_count = 0
for net in new_networks:
12 months ago
net_type = net['type']
if net_type == self.VMNetType.EMPTY.value:
1 year ago
empty_new_nets_count += 1
net_id = empty_new_nets_count
else:
net_id = net['id']
net_key = f'{net_type}{net_id}'
12 months ago
# If DPDK iface MTU is new then add postfix
if net_type == self.VMNetType.DPDK.value:
net_mtu = net['mtu']
if net_mtu is not None:
iface = ifaces_dict.get(net_key)
if iface and net_mtu != iface['mtu']:
net_key = f'{net_key}_new-mtu'
1 year ago
new_nets_dict[net_key] = net
# The networks that no need to be disconnected or reconnected
unchangeable_nets_dict = {}
# Changing with taking into account sequence of
# target network list
if order_changing:
# We proceed from the fact that
# ifaces is sorted by PCI slot number in
# method self._compute_get_by_id
# No need to change networks
if list(ifaces_dict.keys()) == list(new_nets_dict.keys()):
unchangeable_nets_dict = new_nets_dict
# Need to change networks
else:
ifaces_for_delete = ifaces
nets_for_attach = new_networks
# Changing without taking into account sequence of
# target network list (the faster way than with)
else:
# Adding interfaces for delete
for iface_net_key, iface in ifaces_dict.items():
if iface_net_key not in new_nets_dict.keys():
ifaces_for_delete.append(iface)
# Adding networks for attach
for new_net_key, net in new_nets_dict.items():
if new_net_key in ifaces_dict.keys():
unchangeable_nets_dict[new_net_key] = net
else:
nets_for_attach.append(net)
for net_key, net in unchangeable_nets_dict.items():
7 months ago
# Adding networks for change IP address
1 year ago
if net['type'] in ('VINS', 'EXTNET'):
7 months ago
current_ip = ifaces_dict[net_key]['ipAddress']
1 year ago
new_ip = net['ip_addr']
7 months ago
if new_ip and current_ip != new_ip:
1 year ago
nets_for_change_ip.append(net)
7 months ago
# 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
5 months ago
# Adding networks for change MTU
if net['type'] == self.VMNetType.EXTNET.value:
current_mtu = ifaces_dict[net_key]['mtu']
new_mtu = net['mtu']
if new_mtu and current_mtu != new_mtu:
nets_for_change_mtu_dict[net_key] = net
3 weeks ago
# Adding networks for change security groups
current_secgroups_mode = ifaces_dict[net_key][
'enable_secgroups'
]
new_secgroups_mode = net['security_group_mode']
current_secgroup_ids = ifaces_dict[net_key][
'security_groups'
]
new_secgroup_ids = net['security_group_ids']
if (
(
new_secgroups_mode is not None
and current_secgroups_mode != new_secgroups_mode
) or (
new_secgroup_ids
and set(current_secgroup_ids) != set(new_secgroup_ids)
)
):
nets_for_change_sec_groups_dict[net_key] = net
# Adding networks for change state
if (
net['enabled'] is not None
and ifaces_dict[net_key]['enabled'] != net['enabled']
):
nets_for_change_state_dict[net_key] = net
1 year ago
# Detaching networks
for iface in ifaces_for_delete:
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/netDetach',
arg_params={
'computeId': vm_id,
'mac': iface['mac'],
'ipAddr': iface['ipAddress'] or None,
},
)
self.set_changed()
1 year ago
# Attaching networks
for net in nets_for_attach:
3 weeks ago
security_group_mode = net.get('security_group_mode')
if security_group_mode is None:
security_group_mode = False
self.message(
msg=self.MESSAGES.default_value_used(
param_name='networks.security_group_mode',
default_value=False
),
warning=True,
)
enabled = net.get('enabled')
if enabled is None:
enabled = True
self.message(
msg=self.MESSAGES.default_value_used(
param_name='networks.enabled',
default_value=True
),
warning=True,
)
5 months ago
api_params = {
'computeId': vm_id,
'netType': net['type'],
'netId': net.get('id') or 0,
'ipAddr': net.get('ip_addr'),
'mtu': net.get('mtu'),
'mac_addr': net.get('mac'),
3 weeks ago
'security_groups': net.get('security_group_ids'),
'enable_secgroups': security_group_mode,
'enabled': enabled,
5 months ago
}
if net['type'] == self.VMNetType.SDN.value:
api_params['sdn_interface_id'] = net['id']
api_params['netId'] = 0
1 year ago
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/netAttach',
5 months ago
arg_params=api_params,
1 year ago
)
self.set_changed()
1 year ago
# Changing IP adresses
for net in nets_for_change_ip:
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/changeIp',
arg_params={
7 months ago
'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'],
1 year ago
},
)
self.set_changed()
5 months ago
# Changing MTU
for net_key, net in nets_for_change_mtu_dict.items():
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/change_mtu',
arg_params={
'compute_id': vm_id,
'interface': ifaces_dict[net_key]['mac'],
'mtu': net['mtu'],
},
)
self.set_changed()
3 weeks ago
# Changing security groups
if nets_for_change_sec_groups_dict:
for net_key, net in nets_for_change_sec_groups_dict.items():
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/change_security_groups', # noqa: E501
arg_params={
'compute_id': vm_id,
'interface': ifaces_dict[net_key]['mac'],
'security_groups': net['security_group_ids'],
'enable_secgroups': net['security_group_mode'],
},
)
self.set_changed()
# Changing state
if nets_for_change_state_dict:
for net_key, net in nets_for_change_state_dict.items():
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/changeLinkState', # noqa: E501
arg_params={
'computeId': vm_id,
'interface': ifaces_dict[net_key]['mac'],
'state': 'on' if net['enabled'] else 'off',
},
)
self.set_changed()
def compute_resize_vector(self, comp_dict, new_cpu, new_ram):
"""Check if the Compute new size parameters passed to this function are different from the
current Compute configuration.
This method is intended to be called to see if the Compute would be resized in the course
of module run, as sometimes resizing may happen implicitly (e.g. when state = present and
the specified size is different from the current configuration of per-existing target VM.
@param (dict) comp_dict: dictionary of the Compute parameters as returned by previous call
to compute_find(...).
@param (int) new_cpu: new CPU count.
@param (int) new_ram: new RAM size in MBs.
@return: VM_RESIZE_NOT if no change required, VM_RESIZE_DOWN if sizing down (this will
require Compute to be in one of the stopped states), VM_RESIZE_UP if sizing Compute up
(no guest OS restart is generally required for the majority of modern OS-es).
"""
# NOTE: This method may eventually be deemed as redundant and as such may be removed.
if comp_dict['cpus'] == new_cpu and comp_dict['ram'] == new_ram:
return DecortController.VM_RESIZE_NOT
if comp_dict['cpus'] < new_cpu or comp_dict['ram'] < new_ram:
return DecortController.VM_RESIZE_UP
if comp_dict['cpus'] > new_cpu or comp_dict['ram'] > new_ram:
return DecortController.VM_RESIZE_DOWN
return DecortController.VM_RESIZE_NOT
def compute_resource_check(self):
"""Check available resources (in case limits are set on the target VDC and/or account) to make sure
that this Compute instance can be deployed.
@return: True if enough resources, False otherwise.
@return: Dictionary of remaining resources estimation after the specified Compute instance would
have been deployed.
"""
# self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "compute_resource_check")
#
# TODO - This method is under construction
#
return
def compute_restore(self, comp_id):
"""Restores a deleted Compute instance identified by ID.
@param compid: ID of the Compute to restore.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "compute_restore")
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = "compute_restore() in check mode: restore Compute ID {} was requested.".format(comp_id)
return
7 months ago
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
self.result['changed'] = True
return
def compute_resize(self, comp_dict, new_cpu, new_ram, wait_for_state_change=0):
"""Resize existing VM.
@param (dict) comp_dict: dictionary with the facts about the Compute to resize.
@param (int) new_cpu: new vCPU count to set.
@param (int) ram: new RAM size in MB.
@param (int) wait_for_state_change: integer number that tells how many 5 seconds intervals to wait
for the Compute instance power state to change so that the resize operation can be carried out. Set
this to non zero value if you expect that the state of Compute will change shortly (usually, when
you call this method after compute_powerstate(...))
"""
#
# TODO: need better cooperation from OS image attributes as returned by API "images/list".
# Now it is assumed that VM hot resize up is always possible, while hot resize down is not.
#
INVALID_STATES_FOR_HOT_DOWNSIZE = ["RUNNING", "MIGRATING", "DELETED"]
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "compute_resize")
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = ("compute_resize() in check mode: resize of Compute ID {} from CPU:RAM {}:{} to {}:{} "
"was requested.").format(comp_dict['id'],
comp_dict['cpus'], comp_dict['ram'],
new_cpu, new_ram)
return
# We need to handle a situation when either of 'cpu' or 'ram' parameter was not supplied. This is acceptable
# when we manage state of the VM or request change to only one parameter - cpu or ram.
# In such a case take the "missing" value from the current configuration of the VM.
if not new_cpu and not new_ram:
# if both are 0 or Null - return immediately, as user did not mean to manage size
self.result['failed'] = False
4 years ago
self.result['warning'] = ("compute_resize: new CPU count and RAM size are both zero for Compute ID {}"
" - nothing to do.").format(comp_dict['id'])
return
if not new_cpu:
new_cpu = comp_dict['cpus']
elif not new_ram:
new_ram = comp_dict['ram']
if comp_dict['cpus'] == new_cpu and comp_dict['ram'] == new_ram:
# no need to call API in this case, as requested size is not different from the current one
self.result['failed'] = False
4 years ago
self.result['warning'] = ("compute_resize: new CPU count and RAM size are the same for Compute ID {}"
" - nothing to do.").format(comp_dict['id'])
return
if ((comp_dict['cpus'] > new_cpu or comp_dict['ram'] > new_ram) and
4 years ago
comp_dict['status'] in INVALID_STATES_FOR_HOT_DOWNSIZE):
while wait_for_state_change:
time.sleep(5)
fresh_comp_dict = self.compute_find(arg_vm_id=comp_dict['id'])
comp_dict['status'] = fresh_comp_dict['status']
if fresh_comp_dict['status'] not in INVALID_STATES_FOR_HOT_DOWNSIZE:
break
wait_for_state_change = wait_for_state_change - 1
if not wait_for_state_change:
self.result['failed'] = True
self.result['msg'] = ("compute_resize(): downsize of Compute ID {} from CPU:RAM {}:{} to {}:{} was "
4 years ago
"requested, but its current state '{}' is incompatible with downsize operation."). \
format(comp_dict['id'],
comp_dict['cpus'], comp_dict['ram'],
new_cpu, new_ram, comp_dict['status'])
return
api_params = dict(computeId=comp_dict['id'],
ram=new_ram,
4 years ago
cpu=new_cpu, )
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/resize", 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
comp_dict['cpus'] = new_cpu
comp_dict['ram'] - new_ram
return
def compute_wait4state(self, comp_id, pwstate, num_checks=6, sleep_time=5):
"""Helper method to wait for the Compute instance to enter the specified state. Intended
usage of this method is check if specified Compute is already in HALTED state after
calling compute_powerstate(...), as compute_powerstate() may return before Compute instance
actually enters the target state.
@param (int) comp_id: ID of the Compute to monitor.
@param (string) pwstate: target powerstate of the Compute. Make sure that you specify valid
state, or the method will return immediately with False.
@param num_checks: how many check attempts to take before giving up and returning False.
@param sleep_time: sleep time in seconds between checks.
@return: True if the target state is detected within the specified number of checks. False
otherwise or if an invalid target state is specified.
Note: this method will abort module execution if no target VM is found.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "compute_wait4state")
validated_comp_id, comp_dict, _ = self.compute_find(comp_id)
if not validated_comp_id:
self.result['failed'] = True
self.result['msg'] = "Cannot find the specified Compute ID {}".format(comp_id)
self.amodule.fail_json(**self.result)
if pwstate not in ['RUNNING', 'PAUSED', 'HALTED', 'DELETED', 'DESTROYED']:
self.result['warning'] = "compute_wait4state: invalid target state '{}' specified.".format(pwstate)
return False
if comp_dict['status'] == pwstate:
return True
if sleep_time < 1:
sleep_time = 1
for _ in range(0, num_checks):
time.sleep(sleep_time)
_, comp_dict, _ = self.compute_find(comp_id)
if comp_dict['status'] == pwstate:
return True
return False
4 years ago
3 weeks ago
@waypoint
def compute_affinity(self,comp_dict,tags,aff,aaff,label):
"""
Manage Compute Tags,Affinitylabel and rules
@param (dict) comp_dict: dictionary of the Compute parameters
@param (dict) tags: dictionary of the tags
@param (list) aff: affinity rules
@param (list) aaff: antiaffinity rules
@param (str) label: affinity group label
"""
3 weeks ago
if self.amodule.check_mode:
method_kwargs = {}
if aff:
method_kwargs['affinity'] = aff
if aaff:
method_kwargs['anti-affinity'] = aaff
if tags:
method_kwargs['tags'] = tags
if label:
method_kwargs['label'] = label
if method_kwargs:
self.message(self.MESSAGES.method_in_check_mode('compute_affinity()', (), method_kwargs))
return
1 year ago
if tags is not None:
if tags:
for tag in tags.items():
if tag not in comp_dict['tags'].items():
api_params = dict(computeId=comp_dict['id'],
key=tag[0],
value=tag[1], )
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/tagAdd", api_params)
self.result['failed'] = False
self.result['changed'] = True
if comp_dict['tags']:
for tag in comp_dict['tags'].items():
if tag not in tags.items():
api_params = dict(computeId=comp_dict['id'],
key=tag[0],)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/tagRemove", api_params)
self.result['failed'] = False
self.result['changed'] = True
else:
if comp_dict['tags']:
for tag in comp_dict['tags'].items():
api_params = dict(computeId=comp_dict['id'],
1 year ago
key=tag[0],)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/tagRemove", api_params)
self.result['failed'] = False
self.result['changed'] = True
1 year ago
if label is not None:
if label:
if label != comp_dict['affinityLabel']:
api_params = dict(computeId=comp_dict['id'],
1 year ago
affinityLabel=label,)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/affinityLabelSet", api_params)
self.result['failed'] = False
self.result['changed'] = True
else:
if comp_dict['affinityLabel'] != "":
api_params = dict(computeId=comp_dict['id'])
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/affinityLabelRemove", api_params)
self.result['failed'] = False
self.result['changed'] = True
4 years ago
affrule_del = []
affrule_add = []
aaffrule_del = []
aaffrule_add = []
#AFFINITY
1 year ago
if aff is not None:
if comp_dict['affinityRules']:
# raise ValueError(f'affinityRules={comp_dict["affinityRules"]}, aff={aff}')
for rule in comp_dict['affinityRules']:
del rule['guid']
if not aff or rule not in aff:
affrule_del.append(rule)
if aff:
for rule in aff:
if rule not in comp_dict['affinityRules']:
affrule_add.append(rule)
#ANTI AFFINITY
1 year ago
if aaff is not None:
if comp_dict['antiAffinityRules']:
for rule in comp_dict['antiAffinityRules']:
del rule['guid']
if not aaff or rule not in aaff:
aaffrule_del.append(rule)
if aaff:
for rule in aaff:
if rule not in comp_dict['antiAffinityRules']:
aaffrule_add.append(rule)
#AFFINITY
if len (affrule_del):
for rule in affrule_del:
api_params = dict(computeId=comp_dict['id'],
key=rule['key'],
value=rule['value'],
topology=rule['topology'],
mode=rule['mode'],
policy=rule['policy'],)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/affinityRuleRemove", api_params)
self.result['failed'] = False
self.result['changed'] = True
if len(affrule_add)>0:
for rule in affrule_add:
api_params = dict(computeId=comp_dict['id'],
key=rule['key'],
value=rule['value'],
topology=rule['topology'],
mode=rule['mode'],
policy=rule['policy'],)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/affinityRuleAdd", api_params)
self.result['failed'] = False
self.result['changed'] = True
#ANTI AFFINITY
if len(aaffrule_del):
for rule in aaffrule_del:
api_params = dict(computeId=comp_dict['id'],
key=rule['key'],
value=rule['value'],
topology=rule['topology'],
mode=rule['mode'],
policy=rule['policy'],)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/antiAffinityRuleRemove", api_params)
self.result['failed'] = False
self.result['changed'] = True
if len(aaffrule_add)>0:
for rule in aaffrule_add:
api_params = dict(computeId=comp_dict['id'],
key=rule['key'],
value=rule['value'],
topology=rule['topology'],
mode=rule['mode'],
policy=rule['policy'],)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/antiAffinityRuleAdd", api_params)
self.result['failed'] = False
self.result['changed'] = True
return
1 year ago
@waypoint
@checkmode
1 year ago
def compute_update(
self,
compute_id: int,
name: Optional[str] = None,
chipset: Optional[str] = None,
cpu_pin: Optional[bool] = None,
hp_backed: Optional[bool] = None,
numa_affinity: Optional[str] = None,
description: Optional[str] = None,
12 months ago
auto_start: Optional[bool] = None,
10 months ago
preferred_cpu_cores: list[int] | None = None,
7 months ago
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,
3 weeks ago
os_version: None | str = None,
1 year ago
):
1 year ago
OBJ = 'compute'
1 year ago
self.decort_api_call(
1 year ago
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/update',
arg_params={
'computeId': compute_id,
'name': name,
1 year ago
'chipset': chipset,
'cpupin': cpu_pin,
'hpBacked': hp_backed,
'numaAffinity': numa_affinity,
'desc': description,
12 months ago
'autoStart': auto_start,
10 months ago
'preferredCpu': (
[-1] if preferred_cpu_cores == [] else preferred_cpu_cores
),
7 months ago
'bootType': boot_mode,
'loaderType': boot_loader_type,
'networkInterfaceNaming': network_interface_naming,
'hotResize': hot_resize,
3 weeks ago
'os_version': os_version,
1 year ago
},
)
self.set_changed()
1 year ago
params_to_check = {
'name': name,
'chipset': chipset,
'cpu_pin': cpu_pin,
'hp_backed': hp_backed,
'numa_affinity': numa_affinity,
'description': description,
12 months ago
'auto_start': auto_start,
10 months ago
'preferred_cpu_cores': preferred_cpu_cores,
7 months ago
'boot_mode': boot_mode,
'loader_type': boot_loader_type,
'network_interface_naming': network_interface_naming,
'hot_resize': hot_resize,
3 weeks ago
'os_version': os_version,
1 year ago
}
for param, value in params_to_check.items():
if value is not None:
self.message(
self.MESSAGES.obj_smth_changed(
obj=OBJ,
id=compute_id,
smth=param,
new_value=value,
)
1 year ago
)
1 year ago
@waypoint
@checkmode
def compute_set_custom_fields(
self,
compute_id: int,
custom_fields: dict,
):
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/setCustomFields',
arg_params={
'computeId': compute_id,
'customFields': json.dumps(custom_fields),
},
)
self.set_changed()
@waypoint
@checkmode
def compute_disable_custom_fields(
self,
compute_id: int,
):
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/deleteCustomFields',
arg_params={
'computeId': compute_id,
},
)
self.set_changed()
@waypoint
def compute_get_custom_fields(self, compute_id: int) -> Optional[dict]:
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/getCustomFields',
arg_params={
'computeId': compute_id
},
not_fail_codes=[404],
)
if api_resp.status_code == 404:
error_msg = api_resp.json()['error']
if 'customFields' in error_msg:
return None
else:
self.message(error_msg)
self.exit(fail=True)
return api_resp.json()
1 year ago
10 months ago
@waypoint
@checkmode
def compute_rollback(self, compute_id: int, snapshot_label: str):
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/snapshotRollback',
arg_params={
'computeId': compute_id,
'label': snapshot_label,
},
)
self.set_changed()
9 months ago
@waypoint
@checkmode
def compute_clone(
self,
compute_id: int,
name: str,
3 weeks ago
storage_policy_id: int,
9 months ago
force: bool = False,
3 weeks ago
sep_pool_name: str | None = None,
sep_id: int | None = None,
snapshot_datetime: str | None = None,
snapshot_name: str | None = None,
snapshot_timestamp: int | None = None,
9 months ago
):
_snapshot_timestamp = snapshot_timestamp
if snapshot_datetime:
_snapshot_timestamp = self.dt_str_to_sec(dt_str=snapshot_datetime)
api_params = {
'computeId': compute_id,
'force': force,
3 weeks ago
'name': name,
'pool_name': sep_pool_name,
'sep_id': sep_id,
9 months ago
'snapshotName': snapshot_name,
'snapshotTimestamp': _snapshot_timestamp,
3 weeks ago
'storage_policy_id': storage_policy_id,
9 months ago
}
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/clone',
arg_params=api_params,
)
3 weeks ago
audit_id = api_resp.content
task_link = (
f'{self.controller_url}/portal/#/system/tasks/{audit_id}'
)
clone_id = None
task_schedule_timeout = 600
clone_timeout = 1200
sleep_interval = 5
while True:
task_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/tasks/get',
arg_params={'auditId': audit_id},
)
task_resp_data = task_response.json()
match task_resp_data['status']:
case 'SCHEDULED':
if task_schedule_timeout <= 0:
self.message(
'Time to schedule task to clone '
f'VM ID {compute_id} has been exceeded.'
f'\nTask details: {task_link}'
)
self.exit(fail=True)
task_schedule_timeout -= sleep_interval
case 'PROCESSING':
if clone_timeout <= 0:
self.message(
f'Time to clone VM ID {compute_id} '
f'has been exceeded.'
f'\nTask details: {task_link}'
)
self.exit(fail=True)
clone_timeout -= sleep_interval
case 'ERROR':
self.result['msg'] = (
f'Cloning VM ID {compute_id} failed: '
f'{task_resp_data.get("error")}.'
f'\nTask details: {task_link}'
)
self.exit(fail=True)
case 'OK':
clone_id, _, _ = self.compute_find(
comp_name=name,
rg_id=self.rg_id,
)
self.message(
f'Clone ID {clone_id} from VM ID {compute_id} '
'created successfully'
)
self.set_changed()
break
time.sleep(sleep_interval)
return clone_id
9 months ago
@waypoint
def compute_get_console_url(self, compute_id: int):
api_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/getConsoleUrl',
arg_params={
'computeId': compute_id,
},
)
return api_response.text
5 months ago
@waypoint
@checkmode
def compute_migrate_to_zone(self, compute_id: int, zone_id: int):
api_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/migrateToZone',
arg_params={
'computeId': compute_id,
'zoneId': zone_id,
},
)
self.set_changed()
return api_response.json()
@waypoint
def compute_get_snapshot_merge_status(self, vm_id: int) -> dict:
api_response = self.decort_api_call(
arg_req_function=requests.get,
arg_api_name='/restmachine/cloudapi/compute/shared_snapshot_merge_status', # noqa: E501
arg_params={
'compute_id': vm_id,
},
)
return api_response.json()
@waypoint
@checkmode
def compute_guest_agent_disable(self, vm_id: int):
"""
Implementation of functionality of the API method
`/cloudapi/compute/guest_agent_disable`.
"""
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/guest_agent_disable',
arg_params={
'compute_id': vm_id,
}
)
self.set_changed()
return response.json()
@waypoint
@checkmode
def compute_guest_agent_enable(self, vm_id: int):
"""
Implementation of functionality of the API method
`/cloudapi/compute/guest_agent_enable`.
"""
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/guest_agent_enable',
arg_params={
'compute_id': vm_id,
}
)
self.set_changed()
return response.json()
@waypoint
def compute_guest_agent_feature_get(self, vm_id: int) -> list:
"""
Implementation of functionality of the API method
`/cloudapi/compute/guest_agent_feature_get`.
"""
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/guest_agent_feature_get', # noqa: E501
arg_params={
'compute_id': vm_id,
}
)
return response.json()
@waypoint
@checkmode
def compute_guest_agent_feature_update(self, vm_id: int):
"""
Implementation of functionality of the API method
`/cloudapi/compute/guest_agent_feature_update`.
"""
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/guest_agent_feature_update', # noqa: E501
arg_params={
'compute_id': vm_id,
}
)
self.set_changed()
return response.json()
@waypoint
@checkmode
def compute_guest_agent_execute(
self,
vm_id: int,
cmd: str,
args: dict | None = None,
):
"""
Implementation of functionality of the API method
`/cloudapi/compute/guest_agent_execute`.
"""
api_params = {
'compute_id': vm_id,
'command': cmd,
'arguments': json.dumps(args or {}),
}
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/guest_agent_execute',
arg_params=api_params,
)
self.set_changed()
return response.json()['return']
3 weeks ago
@waypoint
@checkmode
def compute_cd_eject(
self,
vm_id: int,
):
"""
Implementation of functionality of the API method
`/cloudapi/compute/cdEject`.
"""
api_params = {
'computeId': vm_id,
}
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/cdEject',
arg_params=api_params,
)
self.set_changed()
return response.json()
@waypoint
@checkmode
def compute_cd_insert(
self,
vm_id: int,
image_id: int,
):
"""
Implementation of functionality of the API method
`/cloudapi/compute/cdInsert`.
"""
api_params = {
'computeId': vm_id,
'cdromId': image_id,
}
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/cdInsert',
arg_params=api_params,
)
self.set_changed()
return response.json()
@waypoint
@checkmode
def compute_set_boot_order(
self,
vm_id: int,
order: list[VMBootDevice],
):
"""
Implementation of functionality of the API method
`/cloudapi/compute/cdInsert`.
"""
api_params = {
'computeId': vm_id,
'order': order,
}
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/bootOrderSet',
arg_params=api_params,
)
self.set_changed()
return response.json()
@waypoint
@checkmode
def compute_disk_redeploy(
self,
vm_id: int,
storage_policy_id: int,
image_id: None | int,
disk_size: None | int,
os_version: None | str,
auto_start: bool = False,
):
"""
Implementation of functionality of the API method
`/cloudapi/compute/redeploy`.
"""
api_params = {
'computeId': vm_id,
'storage_policy_id': storage_policy_id,
'imageId': image_id,
'diskSize': disk_size,
'dataDisks': 'KEEP',
'autoStart': auto_start,
'forceStop': True,
'os_version': os_version,
}
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/redeploy',
arg_params=api_params,
)
self.set_changed()
return response.json()
@waypoint
@checkmode
def compute_clone_abort(
self,
vm_id: int,
):
"""
Implementation of functionality of the API method
`/cloudapi/compute/clone_abort`.
"""
api_params = {
'compute_id': vm_id,
}
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/clone_abort',
arg_params=api_params,
)
self.set_changed()
return response.json()
@waypoint
@checkmode
def compute_get_clone_status(
self,
vm_id: int,
):
"""
Implementation of functionality of the API method
`/cloudapi/compute/clone_status`.
"""
api_params = {
'compute_id': vm_id,
}
response = self.decort_api_call(
arg_req_function=requests.get,
arg_api_name='/restmachine/cloudapi/compute/clone_status',
arg_params=api_params,
)
return response.json()
###################################
# OS image manipulation methods
###################################
def _image_get_by_id(self, image_id):
# TODO: update once cloudapi/image/get is implemented, see ticket #2963
api_params = dict(imageId=image_id,
3 weeks ago
showAll=True)
1 year ago
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/image/get',
arg_params=api_params,
not_fail_codes=[404],
)
if api_resp.status_code == 404:
3 weeks ago
self.message(f'Image with ID {image_id} not found.')
self.exit(fail=True)
1 year ago
return image_id, api_resp.json()
3 weeks ago
def image_find(self, image_id, image_name='', account_id=0, rg_id=0, sepid=0, pool=""):
"""Locates image specified by name and returns its facts as dictionary.
Primary use of this function is to obtain the ID of the image identified by its name and,
3 years ago
optionally SEP ID and/or pool name. Also note that only images in status CREATED are
returned.
@param (string) image_id: ID of the OS image to find. If non-zero ID is specified, then
image_name is ignored.
@param (string) image_name: name of the OS image to find. This argument is ignored if non-zero
image ID is passed.
3 years ago
@param (int) account_id: ID of the account for which the image will be looked up. If set to 0,
the account ID will be obtained from the specified RG ID.
@param (int) rg_id: ID of the RG to use as a reference when listing OS images. This argument is
ignored if non-zero image id and/or non-zero account_id are specified.
3 years ago
@param (int) sepid: ID of the SEP where the image should be present. If set to 0, there will be no
filtering by SEP ID and the first matching image will be returned.
3 years ago
@param (string) pool: name of the pool where the image should be present. If set to empty string, there
will be no filtering by pool name and first matching image will be returned.
3 years ago
@return: image ID and dictionary with image specs. If no matching image found, 0 for ID and None for
dictionary are returned, and self.result['failed']=True.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "image_find")
if image_id > 0:
1 year ago
return self._image_get_by_id(image_id)
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:
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']
api_params = dict(accountId=validated_acc_id)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/image/list", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json.
images_list = json.loads(api_resp.content.decode('utf8'))
for image_record in images_list['data']:
if image_record['name'] == image_name and image_record['status'] == "CREATED":
if sepid == 0 and pool == "":
3 years ago
# if no filtering by SEP ID or pool name is requested, return the first match
3 weeks ago
return self._image_get_by_id(image_record['id'])
# if positive SEP ID and/or non-emtpy pool name are passed, match by them
full_match = True
if sepid > 0 and sepid != image_record['sepId']:
full_match = False
if pool != "" and pool != image_record['pool']:
full_match = False
if full_match:
3 weeks ago
return self._image_get_by_id(image_record['id'])
self.result['failed'] = False
self.result['msg'] = ("Failed to find OS image by name '{}', SEP ID {}, pool '{}' for "
"account ID '{}'.").format(image_name,
4 years ago
sepid, pool,
account_id)
return 0, None
4 years ago
def virt_image_find(self, image_id, virt_name, account_id, rg_id=0, sepid=0, pool=""):
"""Locates virtual image specified by name and returns its facts as dictionary.
Primary use of this function is to obtain the ID of the image identified by its name and,
3 years ago
optionally SEP ID and/or pool name. Also note that only virtual images in status CREATED are
returned.
@param (string) image_id: ID of the OS image to find. If non-zero ID is specified, then
virt_name is ignored.
@param (string) virt_name: name of the OS image to find. This argument is ignored if non-zero
image ID is passed.
3 years ago
@param (int) account_id: ID of the account for which the image will be looked up. If set to 0,
the account ID will be obtained from the specified RG ID.
@param (int) rg_id: ID of the RG to use as a reference when listing OS images. This argument is
ignored if non-zero image id and/or non-zero account_id are specified.
3 years ago
@param (int) sepid: ID of the SEP where the image should be present. If set to 0, there will be no
filtering by SEP ID and the first matching image will be returned.
3 years ago
@param (string) pool: name of the pool where the image should be present. If set to empty string, there
will be no filtering by pool name and first matching image will be returned.
3 years ago
@return: image ID and dictionary with image specs. If no matching image found, 0 for ID and None for
dictionary are returned, and self.result['failed']=True.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "virt_image_find")
3 years ago
if image_id > 0:
ret_image_id, ret_image_dict = self._image_get_by_id(image_id)
if (ret_image_id and
(sepid == 0 or sepid == ret_image_dict['sepId']) and
(pool == "" or pool == ret_image_dict['pool'])):
return ret_image_id, ret_image_dict
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:
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']
api_params = dict(accountId=validated_acc_id)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/image/list", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json.
images_list = json.loads(api_resp.content.decode('utf8'))
for image_record in images_list['data']:
if image_record['name'] == virt_name and image_record['status'] == "CREATED" and image_record['type'] == "virtual":
3 weeks ago
image_id, image_info = self._image_get_by_id(
image_id=image_record['id'],
)
if sepid == 0 and pool == "":
3 years ago
# if no filtering by SEP ID or pool name is requested, return the first match
3 weeks ago
return image_id, image_info
full_match = True
if full_match:
3 weeks ago
return image_id, image_info
self.result['msg'] = ("Failed to find virtual OS image by name '{}', SEP ID {}, pool '{}' for "
"account ID '{}'.").format(virt_name,
sepid, pool,
account_id)
return 0, None
5 months ago
def virt_image_create(
self,
name: str,
target_id: int,
account_id: None | int = 0,
):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "virt_image_create")
5 months ago
api_params = {
'name': name,
'targetId': target_id,
'accountId': account_id,
}
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/image/createVirtual", 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'))
3 years ago
self.result['failed'] = False
self.result['changed'] = True
return 0, None
3 years ago
1 year ago
def image_delete(self, imageId):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "image_delete")
1 year ago
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'))
3 years ago
self.result['changed'] = True
3 weeks ago
return imageId
7 months ago
def image_create(
self,
img_name,
url,
username,
password,
5 months ago
account_id,
7 months ago
usernameDL,
passwordDL,
sepId,
poolName,
3 weeks ago
storage_policy_id: int,
7 months ago
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")
7 months ago
api_params = {
'name': img_name,
'url': url,
'boottype': boot_mode,
'imagetype': boot_loader_type,
5 months ago
'accountId': account_id,
7 months ago
'hotresize': hot_resize,
'username': username,
'password': password,
'usernameDL': usernameDL,
'passwordDL': passwordDL,
'sepId': sepId,
'poolName': poolName,
'networkInterfaceNaming': network_interface_naming,
3 weeks ago
'storage_policy_id': storage_policy_id,
7 months ago
}
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'))
self.result['failed'] = False
self.result['changed'] = True
return 0, None
def virt_image_link(self, imageId, targetId):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "virt_image_link")
api_params = dict(imageId=imageId, targetId=targetId,)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/image/link", 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
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
3 weeks ago
@waypoint
@checkmode
def image_change_storage_policy(
self,
image_id: int,
storage_policy_id: int,
):
"""
Implementation of functionality of the API method
`/cloudapi/image/change_storage_policy`.
"""
api_params = {
'image_id': image_id,
'storage_policy_id': storage_policy_id,
}
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/image/change_storage_policy', # noqa: E501
arg_params=api_params,
)
self.set_changed()
return response.json()
###################################
# 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,
1 year ago
force=recursively,
4 years ago
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):
"""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.
@return: RG ID and a dictionary of RG facts as provided by rg/get
API call. Note that if it fails to find the RG 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_rg_id = 0
ret_rg_dict = dict()
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:
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
)
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, ret_rg_dict
def rg_access(self, arg_rg_id, arg_access):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_access")
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
1 year ago
def rg_find(self, arg_account_id=0, arg_rg_id=0, arg_rg_name="", arg_check_state=True):
"""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.
However, it does fail the run if RG cannot be located by arg_rg_id (if non zero specified) or if API errors
occur.
@param (int) arg_account_id: ID of the account where to look for the RG. Set to 0 if RG is to be located by
its ID.
@param (int) arg_rg_id: integer ID of the RG to be found. If non-zero RG ID is passed, account ID and RG name
are ignored. However, RG must be present in this case, as knowing its ID implies it already exists, otherwise
method will fail.
@param (string) arg_rg_name: string that defines the name of RG to be found. This parameter is case sensitive.
@param (bool) arg_check_state: tells the method to report RGs in valid states only.
@return: ID of the RG, if found. Zero otherwise.
@return: dictionary with RG facts if RG is present. Empty dictionary otherwise. None on error.
"""
# Resource group can be in one of the following states:
# MODELED, CREATED, DISABLING, DISABLED, ENABLING, DELETING, DELETED, DESTROYED, DESTROYED
#
# Transient state (ending with ING) are invalid from RG manipulation viewpoint
#
RG_INVALID_STATES = ["MODELED"]
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_find")
1 year ago
ret_rg_id = 0
api_params = dict()
ret_rg_dict = None
1 year ago
if arg_rg_id is not None and arg_rg_id > 0:
3 weeks ago
api_params['includedeleted'] = True
api_resp = self.decort_api_call(
requests.post,
'/restmachine/cloudapi/rg/list',
api_params,
)
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:
ret_rg_id = got_id
ret_rg_dict = got_specs
break
if not ret_rg_id:
self.result['failed'] = True
self.result['msg'] = "rg_find(): cannot find RG by ID {}.".format(arg_rg_id)
self.amodule.fail_json(**self.result)
3 weeks ago
elif arg_rg_name != "":
# TODO: cloudapi/rg/list will be implemented per Ticket #2848.
# Until then use a three # step approach:
# 1) validate Account ID and obtain the account details (this may fail if user has no
# rights on the specified account);
# 1) get RG list from the specified account;
# 2) try to match RG by name.
if arg_account_id <= 0:
self.result['failed'] = True
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
3 weeks ago
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)
4 years ago
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
# 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:
# Both arg_rg_id and arg_rg_name are empty - there is no way to locate RG in this case
self.result['failed'] = True
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
1 year ago
api_params = {'rgId': arg_rg_id}
if arg_net_type == "NONE":
1 year ago
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
1 year ago
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
5 months ago
def rg_provision(
self,
arg_account_id,
arg_rg_name,
arg_username,
arg_desc,
restype,
arg_net_type,
ipcidr,
arg_extNetId,
arg_extIp,
arg_quota={},
location="",
sdn_access_group_id: None | str = None,
):
"""Provision new RG according to the specified arguments.
If critical error occurs the embedded call to API function will abort further execution of the script
and relay error to Ansible.
Note that RG is created with default network set to NONE, which means that no ViNS is created along
with the new RG.
@param (int) arg_account_id: the non-zero ID of the account under which the new RG will be created.
@param (string) arg_rg_name: the name of the RG to be created.
@param (string) arg_username: the name of the user under DECORT controller, who will have primary
access to the newly created RG.
@param arg_quota: dictionary that defines quotas to set on the RG to be created. Valid keys are: cpu, ram,
disk and ext_ips.
@param (string) location: location code, which identifies the location where RG will be created. If
empty string is passed, the first location under current DECORT controller will be selected.
@param (string) arg_desc: optional text description of this resource group.
@return: ID of the newly created RG (in check mode 0 is returned).
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_provision")
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = ("rg_provision() in check mode: provision RG name '{}' was "
"requested.").format(arg_rg_name)
return 0
target_gid = self.gid_get(location)
if not target_gid:
self.result['failed'] = True
4 years ago
self.result['msg'] = ("rg_provision() failed to obtain valid Grid ID for location '{}'").format(
location)
self.amodule.fail_json(**self.result)
api_params = dict(accountId=arg_account_id,
gid=target_gid,
name=arg_rg_name,
owner=arg_username,
def_net=arg_net_type,
extNetId=arg_extNetId,
extIp=arg_extIp,
5 months ago
sdn_access_group_id=sdn_access_group_id,
)
if arg_quota:
if 'ram' in arg_quota:
api_params['maxMemoryCapacity'] = arg_quota['ram']
if 'disk' in arg_quota:
api_params['maxVDiskCapacity'] = arg_quota['disk']
if 'cpu' in arg_quota:
api_params['maxCPUCapacity'] = arg_quota['cpu']
if 'ext_ips' in arg_quota:
api_params['maxNumPublicIP'] = arg_quota['ext_ips']
if 'net_transfer' in arg_quota:
api_params['maxNetworkPeerTransfer'] = arg_quota['net_transfer']
if restype:
api_params['resourceTypes'] = restype
4 years ago
if arg_desc:
api_params['desc'] = arg_desc
api_params['def_net'] = arg_net_type
if arg_net_type == "PRIVATE" and ipcidr !="":
api_params['ipcidr'] = ipcidr
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/create", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json.
# API /restmachine/cloudapi/rg/create returns ID of the newly created RG on success
self.result['failed'] = False
self.result['changed'] = True
ret_rg_id = int(api_resp.content.decode('utf8'))
return ret_rg_id
# TODO: this method will not work in its current implementation. Update it for new .../rg/update specs.
9 months ago
def rg_update(self, arg_rg_dict, arg_quotas, arg_res_types, 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
call to obtain the data.
@param arg_quotas: dictionary with quota settings. Valid keys are cpu, ram, disk and ext_ips. Not all keys must
be present. Current quota settings for the missing keys will be retained on the RG.
"""
#
# TODO: what happens if user requests quota downsize, and what is currently deployed turns above the new quota?
# TODO: this method may need update since we introduced GPU functionality and corresponding GPU quota management.
#
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_update")
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'])
return
update_required = False
api_params = dict(rgId=arg_rg_dict['id'],)
if arg_res_types:
if arg_rg_dict['resourceTypes'] != arg_res_types:
api_params['resourceTypes'] = arg_res_types
update_required = True
else:
api_params['resourceTypes'] = arg_rg_dict['resourceTypes']
if arg_newname != "" and arg_newname!=arg_rg_dict['name']:
api_params['name'] = arg_newname
update_required = True
# One more inconsistency in API keys:
# - 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_*)
3 weeks ago
query_key_map = dict(
cpu='CU_C',
ram='CU_M',
disk='CU_DM',
ext_ips='CU_I',
net_transfer='CU_NP',
storage_policies='storage_policy',
)
set_key_map = dict(
cpu='maxCPUCapacity',
ram='maxMemoryCapacity',
disk='maxVDiskCapacity',
ext_ips='maxNumPublicIP',
net_transfer='maxNetworkPeerTransfer',
storage_policies='storage_policies',
)
rg_resource_limits_dict = arg_rg_dict['resourceLimits']
for quota_type in (
'cpu', 'ram', 'disk', 'ext_ips',
'net_transfer', 'storage_policies',
):
if arg_quotas:
3 weeks ago
if quota_type in arg_quotas:
# If this resource type limit is found in the desired quotas, check if the desired setting is
# different from the current settings of VDC. If it is different, set the new one.
3 weeks ago
if quota_type == 'storage_policies':
quotas = []
for aparam_storage_policy in arg_quotas[
'storage_policies'
]:
for rg_storage_policy in rg_resource_limits_dict[
'storage_policy'
]:
if (
aparam_storage_policy['id']
== rg_storage_policy['id']
and aparam_storage_policy[
'storage_size_gb'
]
!= rg_storage_policy['limit']
):
update_required = True
quotas.append({
'id': aparam_storage_policy['id'],
'limit': aparam_storage_policy[
'storage_size_gb'
],
})
break
if quotas:
api_params[set_key_map[quota_type]] = (
json.dumps(quotas)
)
else:
if arg_quotas[quota_type] != rg_resource_limits_dict[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]]:
api_params[set_key_map[quota_type]] = arg_quotas[quota_type]
else:
3 weeks ago
api_params[set_key_map[quota_type]] = rg_resource_limits_dict[query_key_map[quota_type]]
else:
# if quotas dictionary is None, it means that no quotas should be set - reset the limits
3 weeks ago
api_params[set_key_map[quota_type]] = rg_resource_limits_dict[query_key_map[quota_type]]
1 year ago
if arg_sep_pools is not None:
if arg_sep_pools:
sep_pools = set()
for sep in arg_sep_pools:
for pool_name in sep['pool_names']:
sep_pools.add(
f'{sep["sep_id"]}_{pool_name}'
)
if set(arg_rg_dict['uniqPools']) != sep_pools:
api_params['uniqPools'] = sep_pools
update_required = True
elif arg_rg_dict['uniqPools']:
api_params['clearUniqPools'] = True
update_required = True
9 months ago
if arg_desc is not None and arg_desc != arg_rg_dict['desc']:
api_params['desc'] = arg_desc
update_required = True
if update_required:
self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/update", 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_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
7 months ago
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
1 year ago
@waypoint
def account_find(
self,
account_name: str = '',
1 year ago
account_id=0,
audits=False,
computes_args: None | dict = None,
disks_args: None | dict = None,
fail_if_not_found=False,
flip_groups_args: None | dict = None,
images_args: None | dict = None,
resource_consumption=False,
resource_groups_args: None | dict = None,
vinses_args: None | dict = None,
):
"""
Find account specified by account ID or name and return
a turple with account ID and account info dict.
@param (int) account_id: ID of the account to find.
@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 (None | dict) computes_args: If dict is
specified, then the method `self.account_computes`
will be called passing founded account ID
and `**computes_args`. Result of the call will
be added to account info dict (key `computes`).
@param (None | dict) disks_args: If dict is
specified, then the method `self.account_disks`
will be called passing founded account ID
and `**disks_args`. Result of the call will
be added to account info dict (key `disks`).
@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 (None | dict) flip_groups_args: If dict is
specified, then the method `self.account_flip_groups`
will be called passing founded account ID
and `**flip_groups_args`. Result of the call will
be added to account info dict (key `flip_groups`).
@param (None | dict) images_args: If dict is
specified, then the method `self.account_images`
will be called passing founded account ID
and `**images_args`. Result of the call will
be added to account info dict (key `images`).
@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`).
@param (None | dict) resource_groups_args: If dict is
specified, then the method `self.account_resource_groups`
will be called passing founded account ID
and `**resource_groups_args`. Result of the call will
be added to account info dict (key `resource_groups`).
@param (None | dict) vinses_args: If dict is
specified, then the method `self.account_vinses`
will be called passing founded account ID
and `**vinses_args`. Result of the call will
be added to account info dict (key `vinses`).
Returns non zero account ID and account info dict on success,
0 and empty dict otherwise (if `fail_if_not_found=False`).
"""
if not account_id and not account_name:
self.result['msg'] = ('Cannot find account if account name and'
' id are not specified.')
self.amodule.fail_json(**self.result)
account_details = None
_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']
for account in accounts_list:
if account['name'] == account_name:
_account_id = account['id']
break
1 year ago
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 = {
'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]
)
if api_resp.status_code == 200:
account_details = api_resp.json()
if not account_details:
if fail_if_not_found:
1 year ago
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')
account_details['vinses_amount'] = account_details.pop('vinses')
7 months ago
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']
)
3 weeks ago
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 = self.account_resource_consumption(
account_id=account_details['id'],
fail_if_not_found=True
)
account_details['resource_consumed'] =\
resource_consumption['Consumed']
account_details['resource_reserved'] =\
resource_consumption['Reserved']
if resource_groups_args is not None:
account_details['resource_groups'] =\
self.account_resource_groups(
account_id=account_details['id'],
**resource_groups_args
)
if computes_args is not None:
account_details['computes'] =\
self.account_computes(
account_id=account_details['id'],
**computes_args
)
if vinses_args is not None:
account_details['vinses'] = self.account_vinses(
account_id=account_details['id'],
**vinses_args
)
if disks_args is not None:
account_details['disks'] =\
self.account_disks(
account_id=account_details['id'],
**disks_args
)
if images_args is not None:
account_details['images'] =\
self.account_images(
account_id=account_details['id'],
**images_args
)
if flip_groups_args is not None:
account_details['flip_groups'] =\
self.account_flip_groups(
account_id=account_details['id'],
**flip_groups_args
)
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_resource_groups(
self,
account_id: int,
page_number: int = 1,
page_size: None | int = None,
rg_id: None | int = None,
rg_name: None | str = None,
rg_status: None | str = None,
sort_by_field: None | str = None,
sort_by_asc=True,
vins_id: None | int = None,
vm_id: None | int = None,
) -> list[dict]:
"""
Implementation of functionality of the API method
`/cloudapi/account/listRG`.
"""
if rg_status and not rg_status in self.RESOURCE_GROUP_STATUSES:
self.result['msg'] = (
f'{rg_status} is not valid RG status for filtering'
f' account resource groups list.'
)
self.amodule.fail_json(**self.result)
sort_by = None
if sort_by_field:
if not sort_by_field in self.FIELDS_FOR_SORTING_ACCOUNT_RG_LIST:
self.result['msg'] = (
f'{sort_by_field} is not valid field for sorting'
f' account resource groups list.'
)
self.amodule.fail_json(**self.result)
sort_by_prefix = '+' if sort_by_asc else '-'
sort_by = f'{sort_by_prefix}{sort_by_field}'
api_params = {
'accountId': account_id,
'name': rg_name,
'page': page_number if page_size else None,
'rgId': rg_id,
'size': page_size,
'sortBy': sort_by,
'status': rg_status,
'vinsId': vins_id,
'vmId': vm_id,
}
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/account/listRG',
arg_params=api_params,
)
resource_groups = api_resp.json()['data']
for rg in resource_groups:
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'])
7 months ago
rg['description'] = rg.pop('desc')
return resource_groups
@waypoint
def account_computes(
self,
account_id: int,
compute_id: None | int = None,
compute_ip: None | str = None,
compute_name: None | str = None,
compute_tech_status: None | str = None,
ext_net_id: None | int = None,
ext_net_name: None | str = None,
page_number: int = 1,
page_size: None | int = None,
rg_id: None | int = None,
rg_name: None | str = None,
sort_by_asc=True,
sort_by_field: None | str = None,
) -> list[dict]:
"""
Implementation of functionality of the API method
`/cloudapi/account/listComputes`.
"""
if compute_tech_status and (
not compute_tech_status in self.COMPUTE_TECH_STATUSES
):
self.result['msg'] = (
f'{compute_tech_status} is not valid compute tech status'
f' for filtering account computes list.'
)
self.amodule.fail_json(**self.result)
sort_by = None
if sort_by_field:
if not sort_by_field in self.FIELDS_FOR_SORTING_ACCOUNT_COMPUTE_LIST:
self.result['msg'] = (
f'{sort_by_field} is not valid field for sorting'
f' account computes list.'
)
self.amodule.fail_json(**self.result)
sort_by_prefix = '+' if sort_by_asc else '-'
sort_by = f'{sort_by_prefix}{sort_by_field}'
api_params = {
'accountId': account_id,
'computeId': compute_id,
'extNetId': ext_net_id,
'extNetName': ext_net_name,
'ipAddress': compute_ip,
'name': compute_name,
'page': page_number if page_size else None,
'rgId': rg_id,
'rgName': rg_name,
'size': page_size,
'sortBy': sort_by,
'techStatus': compute_tech_status,
}
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/account/listComputes',
arg_params=api_params,
)
computes = api_resp.json()['data']
for c in computes:
c['createdTime_readable'] = self.sec_to_dt_str(c['createdTime'])
c['deletedTime_readable'] = self.sec_to_dt_str(c['deletedTime'])
c['updatedTime_readable'] = self.sec_to_dt_str(c['updatedTime'])
return computes
@waypoint
def account_vinses(
self,
account_id: int,
ext_ip: None | str = None,
page_number: int = 1,
page_size: None | int = None,
rg_id: None | int = None,
sort_by_asc=True,
sort_by_field: None | str = None,
vins_id: None | int = None,
vins_name: None | str = None,
) -> list[dict]:
"""
Implementation of functionality of the API method
`/cloudapi/account/listVins`.
"""
sort_by = None
if sort_by_field:
if not sort_by_field in self.FIELDS_FOR_SORTING_ACCOUNT_VINS_LIST:
self.result['msg'] = (
f'{sort_by_field} is not valid field for sorting'
f' account vins list.'
)
self.amodule.fail_json(**self.result)
sort_by_prefix = '+' if sort_by_asc else '-'
sort_by = f'{sort_by_prefix}{sort_by_field}'
api_params = {
'accountId': account_id,
'extIp': ext_ip,
'name': vins_name,
'page': page_number if page_size else None,
'rgId': rg_id,
'size': page_size,
'sortBy': sort_by,
'vinsId': vins_id,
}
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/account/listVins',
arg_params=api_params,
)
vinses = api_resp.json()['data']
for v in vinses:
v['createdTime_readable'] = self.sec_to_dt_str(v['createdTime'])
v['deletedTime_readable'] = self.sec_to_dt_str(v['deletedTime'])
v['updatedTime_readable'] = self.sec_to_dt_str(v['updatedTime'])
return vinses
@waypoint
def account_disks(
self,
account_id: int,
disk_id: None | int = None,
disk_name: None | str = None,
disk_size: None | int = None,
disk_type: None | str = None,
page_number: int = 1,
page_size: None | int = None,
sort_by_asc=True,
sort_by_field: None | str = None,
) -> list[dict]:
"""
Implementation of functionality of the API method
`/cloudapi/account/listDisks`.
"""
if disk_type and (
not disk_type in self.DISK_TYPES
):
self.result['msg'] = (
f'{disk_type} is not valid disk type'
f' for filtering account disk list.'
)
self.amodule.fail_json(**self.result)
sort_by = None
if sort_by_field:
if not sort_by_field in self.FIELDS_FOR_SORTING_ACCOUNT_DISK_LIST:
self.result['msg'] = (
f'{sort_by_field} is not valid field for sorting'
f' account disks list.'
)
self.amodule.fail_json(**self.result)
sort_by_prefix = '+' if sort_by_asc else '-'
sort_by = f'{sort_by_prefix}{sort_by_field}'
api_params = {
'accountId': account_id,
'diskId': disk_id,
'diskMaxSize': disk_size,
'name': disk_name,
'page': page_number if page_size else None,
'size': page_size,
'sortBy': sort_by,
'type': disk_type,
}
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/account/listDisks',
arg_params=api_params,
)
disks = api_resp.json()['data']
return disks
@waypoint
def account_images(
self,
account_id: int,
image_id: None | int = None,
image_name: None | str = None,
image_type: None | str = None,
page_number: int = 1,
page_size: None | int = None,
sort_by_asc=True,
sort_by_field: None | str = None,
) -> list[dict]:
"""
Implementation of functionality of the API method
`/cloudapi/account/listTemplates`.
"""
if image_type and (
not image_type in self.IMAGE_TYPES
):
self.result['msg'] = (
f'{image_type} is not valid image type'
f' for filtering account image list.'
)
self.amodule.fail_json(**self.result)
sort_by = None
if sort_by_field:
if not sort_by_field in self.FIELDS_FOR_SORTING_ACCOUNT_IMAGE_LIST:
self.result['msg'] = (
f'{sort_by_field} is not valid field for sorting'
f' account image list.'
)
self.amodule.fail_json(**self.result)
sort_by_prefix = '+' if sort_by_asc else '-'
sort_by = f'{sort_by_prefix}{sort_by_field}'
api_params = {
'accountId': account_id,
'imageId': image_id,
'name': image_name,
'page': page_number if page_size else None,
'size': page_size,
'sortBy': sort_by,
'type': image_type,
}
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/account/listTemplates',
arg_params=api_params,
)
images = api_resp.json()['data']
return images
@waypoint
def account_flip_groups(
self,
account_id: int,
ext_net_id: None | int = None,
flig_group_id: None | int = None,
flig_group_ip: None | str = None,
flig_group_name: None | str = None,
page_number: int = 1,
page_size: None | int = None,
vins_id: None | int = None,
vins_name: None | str = None,
) -> list[dict]:
"""
Implementation of functionality of the API method
`/cloudapi/account/listFlipGroups`.
"""
api_params = {
'accountId': account_id,
'byIp': flig_group_ip,
'extnetId': ext_net_id,
'flipGroupId': flig_group_id,
'name': flig_group_name,
'page': page_number if page_size else None,
'size': page_size,
'vinsId': vins_id,
'vinsName': vins_name,
}
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/account/listFlipGroups',
arg_params=api_params,
)
flip_groups = api_resp.json()['data']
for fg in flip_groups:
fg['createdTime_readable'] = self.sec_to_dt_str(fg['createdTime'])
fg['deletedTime_readable'] = self.sec_to_dt_str(fg['deletedTime'])
fg['updatedTime_readable'] = self.sec_to_dt_str(fg['updatedTime'])
return flip_groups
@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,
3 weeks ago
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
3 weeks ago
audits = api_resp.json()['data']
for a in audits:
3 weeks ago
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
1 year ago
@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,
},
5 months ago
not_fail_codes=[404],
accept_json_response=True,
1 year ago
)
if api_resp.status_code == 404:
self.message(
self.MESSAGES.obj_not_found(obj=OBJ, id=account_id)
)
self.exit(fail=True)
self.set_changed()
5 months ago
audit_id = api_resp.json()
task_link = (
f'{self.controller_url}/portal/#/system/tasks/{audit_id}'
)
task_schedule_timeout = 600
account_delete_timeout = 1200
sleep_interval = 5
while True:
task_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/tasks/get',
arg_params={'auditId': audit_id},
)
task_resp_data = task_response.json()
match task_resp_data['status']:
case 'SCHEDULED':
if task_schedule_timeout <= 0:
self.message(
f'Time to schedule task to delete account ID '
f'{account_id} has been exceeded.'
f'\nTask details: {task_link}'
)
self.exit(fail=True)
task_schedule_timeout -= sleep_interval
case 'PROCESSING':
if account_delete_timeout <= 0:
self.message(
f'Time to delete account ID {account_id} '
f'has been exceeded.'
f'\nTask details: {task_link}'
)
self.exit(fail=True)
account_delete_timeout -= sleep_interval
case 'ERROR':
self.result['msg'] = (
f'Deleting account ID {account_id} failed:'
f'{task_resp_data.get("error")}.'
f'\nTask details: {task_link}'
)
self.exit(fail=True)
case 'OK':
self.message(
self.MESSAGES.obj_deleted(
obj=OBJ,
id=account_id,
permanently=permanently,
)
)
break
time.sleep(sleep_interval)
1 year ago
@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,
},
5 months ago
not_fail_codes=[404],
accept_json_response=True,
1 year ago
)
if api_resp.status_code == 404:
self.message(
self.MESSAGES.obj_not_found(obj=OBJ, id=account_id)
)
self.exit(fail=True)
self.set_changed()
5 months ago
audit_id = api_resp.json()
task_link = (
f'{self.controller_url}/portal/#/system/tasks/{audit_id}'
)
task_schedule_timeout = 600
account_restore_timeout = 1200
sleep_interval = 5
while True:
task_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/tasks/get',
arg_params={'auditId': audit_id},
)
task_resp_data = task_response.json()
match task_resp_data['status']:
case 'SCHEDULED':
if task_schedule_timeout <= 0:
self.message(
f'Time to schedule task to restore account ID '
f'{account_id} has been exceeded.'
f'\nTask details: {task_link}'
)
self.exit(fail=True)
task_schedule_timeout -= sleep_interval
case 'PROCESSING':
if account_restore_timeout <= 0:
self.message(
f'Time to restore account ID {account_id} '
f'has been exceeded.'
f'\nTask details: {task_link}'
)
self.exit(fail=True)
account_restore_timeout -= sleep_interval
case 'ERROR':
self.result['msg'] = (
f'Restoring account ID {account_id} failed:'
f'{task_resp_data.get("error")}.'
f'\nTask details: {task_link}'
)
self.exit(fail=True)
case 'OK':
self.message(
self.MESSAGES.obj_restored(
obj=OBJ,
id=account_id,
)
)
break
time.sleep(sleep_interval)
1 year ago
@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,
1 year ago
ram_quota: None | int = None,
7 months ago
sep_pools: None | Iterable[str] = None,
5 months ago
description: None | str = None,
default_zone_id: None | int = None,
) -> None:
1 year ago
"""
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,
1 year ago
'uniqPools': sep_pools,
7 months ago
'desc': description,
5 months ago
'defaultZoneId': default_zone_id,
1 year ago
},
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)
)
5 months ago
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,
)
)
1 year ago
self.set_changed()
###################################
# Workflow callback stub methods - not fully implemented yet
###################################
def workflow_cb_set(self, arg_workflow_callback, arg_workflow_context=None):
"""Set workflow callback and workflow context value.
"""
self.workflow_callback = arg_workflow_callback
if arg_workflow_callback != "":
self.workflow_callback_present = True
else:
self.workflow_callback_present = False
if arg_workflow_context != "":
self.workflow_context = arg_workflow_context
else:
self.workflow_context = ""
return
def workflow_cb_call(self):
"""Invoke workflow callback if it was specified earlyer with workflow_cb_set(...) method.
"""
#
# TODO: under construction
#
if self.workflow_callback_present:
pass
return
def run_phase_set(self, arg_phase_name):
"""Set run phase name for module run progress reporting"""
self.run_phase = arg_phase_name
return
def gid_get(self, location_code=""):
"""Get Grid ID for the specified location code.
@param (string) location_code: location code for the Grid ID. If empty string is passed,
the first Grid ID found will be returned.
@returns: integer Grid ID corresponding to the specified location. If no Grid matches specified
location, 0 is returned.
"""
ret_gid = 0
api_params = dict()
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/locations/list", api_params)
4 years ago
if api_resp.status_code == 200:
locations = json.loads(api_resp.content.decode('utf8'))
if location_code == "" and locations:
ret_gid = locations['data'][0]['gid']
else:
2 years ago
for runner in locations['data']:
if runner['locationCode'] == location_code:
# location code matches
ret_gid = runner['gid']
break
return ret_gid
##############################
#
# 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,
4 years ago
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):
"""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.
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)
4 years ago
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)
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
"""
if not rg_id:
self.result['failed'] = True
self.result['msg'] = "_rg_listvins(): zero RG ID specified."
self.amodule.fail_json(**self.result)
api_params = dict(rgId=rg_id, )
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/listVins", api_params)
if api_resp.status_code == 200:
ret_rg_vins_list = json.loads(api_resp.content.decode('utf8'))
else:
self.result['warning'] = ("rg_listvins(): failed to get RG by ID {}. HTTP code {}, "
"response {}.").format(rg_id, api_resp.status_code, api_resp.reason)
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):
"""Find specified ViNS.
@param (int) vins_id: ID of the ViNS. If non-zero vins_id is specified, all other arguments
are ignored, ViNS must exist and is located by its ID only.
@param (string) vins_name: If vins_id is 0, then vins_name is mandatory. Further search for
ViNS is based on combination of account_id and rg_id. If account_id is non-zero, then rg_id
is ignored and ViNS is supposed to exist at account level.
@param (int) account_id: set to non-zero value to search for ViNS by name at this account level.
@param (int) rg_id: set to non-zero value to search for ViNS by name at this RG level. Note, that
in this case account_id should be set to 0.
@param (bool) check_state: tells the method to report ViNSes in valid states only. Set check_state
to False if you want to check if specified ViNS exists at all without failing the module execution.
@returns: ViNS ID and dictionary with ViNS facts. It may return zero ID and empty dictionary
if no ViNS found and check_state=False, so make sure to check return values in the upstream
code accordingly.
"""
# transient and deleted/destroyed states are deemed invalid
3 weeks ago
VINS_INVALID_STATES = ["ENABLING", "DISABLING", "DELETING", "DESTROYING"]
ret_vins_id = 0
3 weeks ago
api_params: dict = {
'includeDeleted': True,
}
ret_vins_facts = None
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_find")
3 weeks ago
if vins_id > 0:
3 weeks ago
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
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
else:
return 0, None
elif vins_name != "":
if rg_id > 0:
# search for ViNS at RG level
# validated_id, validated_facts = self._rg_get_by_id(rg_id)
# if not validated_id:
# self.result['failed'] = True
# self.result['msg'] = "vins_find(): cannot find RG ID {}.".format(rg_id)
# self.amodule.fail_json(**self.result)
# # NOTE: RG's 'vins' attribute does not list destroyed ViNSes!
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
elif account_id > 0:
# search for ViNS at account level
# validated_id, validated_facts = self.account_find("", account_id)
# if not validated_id:
# self.result['failed'] = True
# self.result['msg'] = "vins_find(): cannot find Account ID {}.".format(account_id)
# self.amodule.fail_json(**self.result)
# 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
4 years ago
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)
self.amodule.fail_json(**self.result)
4 years ago
else: # ViNS ID is 0 and ViNS name is emtpy - fail the module
self.result['failed'] = True
self.result['msg'] = "vins_find(): cannot find ViNS by zero ID and empty name."
self.amodule.fail_json(**self.result)
return 0, None
5 months ago
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,
):
"""Provision ViNS according to the specified arguments.
If critical error occurs the embedded call to API function will abort further execution of
the script and relay error to Ansible.
Note, that when creating ViNS at account level, default location under DECORT controller
will be selected automatically.
@param (int) account_id: ID of the account where ViNS will be created. To create ViNS at account
level specify non-zero account ID and zero RG ID.
@param (string) rg_id: ID of the RG where ViNS will be created. If non-zero RG ID is specified,
ViNS will be created at this RG level.
@param (string) ipcidr: optional IP network address to use for internal ViNS network.
@param (int) ext_net_id: optional ID of the external network to connect this ViNS to. Specify -1
to created isolated ViNS, 0 to let platform select default external network, or ID of the network
to ust. Note: this parameter is ignored for ViNS created at account level.
@param (string) ext_ip_addr: optional IP address of the external network connection for this ViNS. If
emtpy string is passed when ext_net_id >= 0, the platform will assign IP address automatically. If
explicitly specified IP address is invalid or already occupied, the method will fail. Note: this
parameter is ignored for ViNS created at account level.
@param (string) desc: optional text description of this ViNS.
@return: ID of the newly created ViNS (in Ansible check mode 0 is returned).
"""
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(
name=vins_name,
accountId=account_id,
gid=target_gid,
)
elif rg_id:
api_url = "/restmachine/cloudapi/vins/createInRG"
api_params = dict(
name=vins_name,
rgId=rg_id,
extNetId=ext_net_id,
)
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
3 weeks ago
api_params['desc'] = desc
5 months ago
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
7 months ago
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):
"""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 desired_state: the desired state for this ViNS. Valid states are 'enabled' and 'disabled'.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_state")
4 years ago
NOP_STATES_FOR_VINS_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING",
"DESTROYED"]
VALID_TARGET_STATES = ["enabled", "disabled"]
if vins_dict['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'])
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,
4 years ago
vins_dict['id'])
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 "
4 years ago
"'{}' was requested.").format(vins_dict['id'], vins_dict['name'],
desired_state)
return
vinsstate_api = "" # this string will also be used as a flag to indicate that API call is necessary
7 months ago
api_params = dict(vinsId=vins_dict['id'])
expected_state = ""
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=""):
"""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
to update such ViNS will have no effect.
@param (dict) vins_dict: dictionary with target ViNS details as returned by vins_find() method.
@param (int) ext_net_id: sets ViNS network connection status. Pass -1 to disconnect ViNS from
external network or positive network ID to connect to the specified external network.
@param (string) ext_ip_addr: optional IP address to assign to the external network connection
of this ViNS.
Note: on success vins_dict may no longer contain actual info about this ViNS, so it is
recommended to update ViNS facts in the upstream code.
"""
4 years ago
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 '{}' "
4 years ago
"was requested.").format(vins_dict['id'], vins_dict['name'])
return
if not vins_dict['rgId']:
# 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'])
return
gw_config = None
if vins_dict['vnfs'].get('GW'):
gw_config = vins_dict['vnfs']['GW']['config']
if 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
# On success the above call will return here. On error it will abort execution by calling fail_json.
elif 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:
# 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
# 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
# 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)
3 weeks ago
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()
4 years ago
else: # 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
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'],
4 years ago
gw_config[
3 years ago
'ext_net_id'])
return
def vins_update_mgmt(self, vins_dict, mgmtaddr=[]):
3 years ago
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'])
return
3 years ago
if self.amodule.params['config_save'] and vins_dict['VNFDev']['customPrecfg']:
# only save config,no other modifictaion
self.result['changed'] = True
self._vins_vnf_config_save(vins_dict['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'])
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'])
3 years ago
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'])
3 years ago
self.result['changed'] = True
self.result['failed'] = False
else:
if vins_dict['VNFDev']['customPrecfg']:
self._vins_vnf_customconfig_set(vins_dict['VNFDev']['id'],False)
3 years ago
self.result['changed'] = True
self.result['failed'] = False
return
def vins_update_ifaces(self,vins_dict,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'])
return
list_ifaces_ip = [rec['ipaddr'] for rec in vinses]
vinsid_not_existed = []
3 years ago
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'])
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))
3 years ago
if not vinses:
return
list_account_vins = self._get_all_account_vinses(vins_dict['VNFDev']['accountId'])
list_account_vinsid = [rec['id'] for rec in list_account_vins]
3 years ago
for vins in vinses:
if vins['id'] in list_account_vinsid:
_,v_dict = self._vins_get_by_id(vins['id'])
#TODO: vins reservation
self._vnf_iface_add(vins_dict['VNFDev']['id'],v_dict['vxlanId'],vins['ipaddr'],vins['netmask'])
self.result['changed'] = True
self.result['failed'] = False
else:
vinsid_not_existed.append(vins['id'])
if vinsid_not_existed:
self.result['warning'] = ("List ViNS id: {} that not created on account id: {}").format(
vinsid_not_existed,
vins_dict['VNFDev']['accountId']
)
return
5 months ago
@waypoint
@checkmode
def vins_migrate_to_zone(self, net_id: int, zone_id: int):
api_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/vins/migrateToZone',
arg_params={
'net_id': net_id,
'zone_id': zone_id,
},
)
self.set_changed()
return api_response.json()
3 years ago
def _vnf_iface_add(self,arg_devid,arg_vxlanid,arg_ipaddr,arg_netmask="24",arg_defgw=""):
api_params = dict(
devId=arg_devid,
ifType="CUSTOM",
connType="VXLAN",
connId=arg_vxlanid,
ipAddr=arg_ipaddr,
netMask=arg_netmask,
defGw=arg_defgw
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/ifaceAdd", api_params)
conn_dict = json.loads(api_resp.content.decode('utf8'))
return
def _vnf_iface_remove(self,arg_devid,arg_iface_name):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "_vnf_iface_add")
api_params = dict(
devId=arg_devid,
name=arg_iface_name,
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/ifaceRemove", api_params)
return
def _get_vnf_by_id(self,vnf_id):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "get_vnf_by_id")
api_params = dict(devId=vnf_id)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/get", api_params)
if api_resp.status_code == 200:
ret_vnf_dict = json.loads(api_resp.content.decode('utf8'))
else:
self.result['warning'] = ("get_all_account_vinses(): failed to configuration of the specified VNF device ID{}. HTTP code {}, "
"response {}.").format(vnf_id, api_resp.status_code, api_resp.reason)
return ret_vnf_dict
def _get_all_account_vinses(self,acc_id):
api_params = dict(accountId=acc_id)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/account/listVins", api_params)
if api_resp.status_code == 200:
ret_listvins_dict = json.loads(api_resp.content.decode('utf8'))
else:
self.result['warning'] = ("get_all_account_vinses(): failed to get list VINS in Account ID {}. HTTP code {}, "
"response {}.").format(acc_id, api_resp.status_code, api_resp.reason)
return []
return ret_listvins_dict['data']
3 years ago
def _vins_vnf_addmgmtaddr(self,dev_id,mgmtip):
api_params = dict(devId=dev_id,ip=mgmtip)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/addMgmtAddr", api_params)
if api_resp.status_code == 200:
self.result['changed'] = True
self.result['failed'] = False
else:
self.result['warning'] = ("_vins_vnf_addmgmtaddr(): failed to add MGMT addr VNFID {} iface ADDR {}. HTTP code {}, "
"response {}.").format(dev_id,mgmtip,api_resp.status_code, api_resp.reason)
return
def _vins_vnf_delmgmtaddr(self,dev_id,mgmtip):
api_params = dict(devId=dev_id,ip=mgmtip)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/delMgmtAddr", api_params)
if api_resp.status_code == 200:
self.result['changed'] = True
self.result['failed'] = False
else:
self.result['warning'] = ("_vins_vnf_delmgmtaddr(): failed to delete MGMT addr VNFID {} iface ADDR {}. HTTP code {}, "
"response {}.").format(dev_id,mgmtip,api_resp.status_code, api_resp.reason)
return
def _vins_vnf_customconfig_set(self,dev_id,arg_mode=True):
api_params = dict(devId=dev_id,mode=arg_mode)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/customSet", api_params)
if api_resp.status_code == 200:
self.result['changed'] = True
self.result['failed'] = False
else:
self.result['warning'] = ("_vins_vnf_customconfig_set(): failed to enable or disable Custom pre-config mode on the VNF device. {}. HTTP code {}, "
"response {}.").format(dev_id,api_resp.status_code, api_resp.reason)
return
def _vins_vnf_config_save(self,dev_id):
api_params = dict(devId=dev_id)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/configSave", api_params)
if api_resp.status_code == 200:
self.result['changed'] = True
self.result['failed'] = False
else:
self.result['warning'] = ("_vins_vnf_config_set(): failed to Save configuration on the VNF device. {}. HTTP code {}, "
"response {}.").format(dev_id,api_resp.status_code, api_resp.reason)
return
##############################
#
# Disk management
#
##############################
def disk_check_iotune_arg(self,iotune_list):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_check_iotune_arg")
MIN_IOPS = 80
total_bytes_sec=iotune_list['total_bytes_sec']
read_bytes_sec=iotune_list['read_bytes_sec']
write_bytes_sec=iotune_list['write_bytes_sec']
total_iops_sec=iotune_list['total_iops_sec']
read_iops_sec=iotune_list['read_iops_sec']
write_iops_sec=iotune_list['write_iops_sec']
total_bytes_sec_max=iotune_list['total_bytes_sec_max']
read_bytes_sec_max=iotune_list['read_bytes_sec_max']
write_bytes_sec_max=iotune_list['write_bytes_sec_max']
total_iops_sec_max=iotune_list['total_iops_sec_max']
read_iops_sec_max=iotune_list['read_iops_sec_max']
write_iops_sec_max=iotune_list['write_iops_sec_max']
size_iops_sec=iotune_list['size_iops_sec']
if total_iops_sec and (read_iops_sec or write_iops_sec):
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] = (f"total and read/write of iops_sec cannot be set at the same time")
if total_bytes_sec and (read_bytes_sec or write_bytes_sec):
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] = (f"total and read/write of bytes_sec cannot be set at the same time")
if total_bytes_sec_max and (read_bytes_sec_max or write_bytes_sec_max):
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] =(f"total and read/write of bytes_sec_max cannot be set at the same time")
if total_iops_sec_max and (read_iops_sec_max or write_iops_sec_max):
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] =(f"total and read/write of iops_sec_max cannot be set at the same time")
for arg, val in iotune_list.items():
if arg in (
"total_iops_sec",
"read_iops_sec",
"write_iops_sec",
"total_iops_sec_max",
"read_iops_sec_max",
"write_iops_sec_max",
"size_iops_sec",
):
if val and val < MIN_IOPS:
self.result['msg'] = (f"{arg} was set below the minimum iops {MIN_IOPS}: {val} provided")
return
def disk_delete(self, disk_id, permanently, detach, reason):
"""Deletes specified Disk.
@param (int) disk_id: ID of the Disk to be deleted.
@param (bool) arg_permanently: a bool that tells if deletion should be permanent. If False, the Disk will be
marked as DELETED and placed into a trash bin for predefined period of time (usually, a few days). Until
this period passes such a Disk can be restored by calling the corresponding 'restore' method.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_delete")
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = "disk_delete() in check mode: delete Disk ID {} was requested.".format(disk_id)
return
api_params = dict(diskId=disk_id,
detach=detach,
7 months ago
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
self.result['changed'] = True
3 weeks ago
return disk_id
def _disk_get_by_id(self, disk_id):
"""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.
"""
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)
4 years ago
api_params = dict(diskId=disk_id, )
3 weeks ago
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'))
3 weeks ago
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)
return ret_disk_id, ret_disk_dict
def disk_find(self, disk_id=0, name="", account_id=0, check_state=False):
"""Find specified Disk.
@param (int) disk_id: ID of the Disk. If non-zero disk_id is specified, all other arguments
are ignored, Disk must exist and is located by its ID only.
@param (string) disk_name: If disk_id is 0, then disk_name is mandatory, and the disk is looked
up in the specified account.
@param (int) account_id: id of the account that owns this disk. Ignored if non-zero disk_id is
specified, but becomes mandatory otherwise.
@param (bool) check_state: tells the method to report Disk(s) in valid states only. Set check_state
to False if you want to check if specified Disk exists at all without failing the module execution.
@returns: Disk ID and dictionary with Disk facts. It may return zero ID and empty dictionary
if no Disk found and check_state=False, so make sure to check return values in the upstream
code accordingly.
"""
4 years ago
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_find")
DISK_INVALID_STATES = ["MODELED", "CREATING", "DELETING", "DESTROYING"]
ret_disk_id = 0
ret_disk_facts = None
if disk_id:
ret_disk_id, ret_disk_facts = self._disk_get_by_id(disk_id)
3 weeks ago
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
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)]
# the above call may return more than one matching disk
if len(disks_list) == 0:
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]
4 years ago
else: # we are missing meaningful account_id - fail the module
self.result['failed'] = True
self.result['msg'] = ("disk_find(): cannot find Disk by name '{}' "
"when no account ID specified.").format(name)
self.amodule.fail_json(**self.result)
4 years ago
else: # Disk ID is 0 and Disk name is emtpy - fail the module
self.result['failed'] = True
self.result['msg'] = "disk_find(): cannot find Disk by zero ID and empty name."
self.amodule.fail_json(**self.result)
return 0, None
3 weeks ago
def disk_create(
self,
accountId,
name,
description,
size,
sep_id,
pool,
storage_policy_id: int,
):
"""Provision Disk according to the specified arguments.
Note that disks created by this method will be of type 'D' (data disks).
If critical error occurs the embedded call to API function will abort further execution
of the script and relay error to Ansible.
@param (string) name: name to assign to the Disk.
@param (int) size: size of the disk in GB.
@param (int) accountId: ID of the account where disk will belong.
@param (int) sep_id: ID of the SEP (Storage Endpoint Provider), where disk will be created.
@param (string) pool: optional name of the pool, where this disk will be created.
@param (string) description: optional text description of this disk.
@param (int) gid: optional Grid id, if specified the disk will be created in selected
location.
@return: ID of the newly created Disk (in Ansible check mode 0 is returned).
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_creation")
3 weeks ago
api_params = dict(
accountId=accountId,
name=name,
description=description,
size=size,
sep_id=sep_id,
pool=pool,
storage_policy_id=storage_policy_id,
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/create", api_params)
if api_resp.status_code == 200:
ret_disk_id = json.loads(api_resp.content.decode('utf8'))
self.result['failed'] = False
self.result['changed'] = True
return ret_disk_id
def disk_resize(self, disk_facts, new_size):
"""Resize Disk. Only increasing disk size is allowed.
@param (dict) disk_dict: dictionary with target Disk details as returned by ../disks/get
API call or disk_find() method.
@param (int) new_size: new size of the disk in GB. It must be greater than current disk
size for the method to succeed.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_resize")
# Values that are specified via Jinja templating engine (e.g. "{{ new_size }}") may come
# as strings. To make sure comparison of new values against current compute size is done
# correcly, we explicitly cast them to type int here.
new_size = int(new_size)
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = ("disk_resize() in check mode: resize Disk ID {} "
"to {} GB was requested.").format(disk_facts['id'], new_size)
return
if not new_size:
self.result['failed'] = False
4 years ago
self.result['warning'] = "disk_resize(): zero size requested for Disk ID {} - ignoring.".format(
disk_facts['id'])
return
if new_size < disk_facts['sizeMax']:
self.result['failed'] = True
self.result['msg'] = ("disk_resize(): downsizing Disk ID {} is not allowed - current "
4 years ago
"size {}, requeste size {}.").format(disk_facts['id'],
disk_facts['sizeMax'], new_size)
return
if new_size == disk_facts['sizeMax']:
self.result['failed'] = False
self.result['warning'] = ("disk_resize(): nothing to do for Disk ID {} - new size is the "
"same as current.").format(disk_facts['id'])
return
api_params = dict(diskId=disk_facts['id'], size=new_size)
# NOTE: we are using API "resize2", as in this module we are managing
# disks attached to compute(s) (DSF ver.2 only)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/resize2", 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
disk_facts['sizeMax'] = new_size
return
def disk_limitIO(self,disk_id, limits):
"""Limits already created Disk identified by its ID.
@param (dict) limits: Dictionary with limits.
@param (int) diskId: ID of the Disk to limit.
@returns: nothing on success. On error this method will abort module execution.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_limitIO")
api_params = dict(diskId=disk_id,
**limits)
self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/limitIO", api_params)
self.result['changed'] = True
self.result['msg'] = "Specified Disk ID {} limited successfully.".format(disk_id)
return
def disk_rename(self, disk_id, name):
"""Renames disk to the specified new name.
@param disk_id: ID of the Disk to rename.
@param name: New name.
@returns: nothing on success. On error this method will abort module execution.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_rename")
api_params = dict(diskId=disk_id,
name=name)
self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/rename", 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
self.result['msg'] = ("Disk with id '{}',successfully renamed to '{}'.").format(disk_id, name)
return
def disk_restore(self, disk_id):
"""Restores previously deleted Disk identified by its ID. For restore to succeed
the Disk must be in 'DELETED' state.
@param disk_id: ID of the Disk to restore.
@returns: nothing on success. On error this method will abort module execution.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_restore")
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = "disk_restore() in check mode: restore Disk ID {} was requested.".format(disk_id)
return
7 months ago
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
self.result['changed'] = True
return
def disk_share(self, disk_id, share='false'):
"""Share data disk
@param disk_id: ID of the Disk to share.
@param share: share status of the disk
@returns: nothing on success. On error this method will abort module execution.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_share")
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = "disk_share() in check mode: share Disk ID {} was requested.".format(disk_id)
return
api_params = dict(diskId=disk_id)
if share:
self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/share", api_params)
else:
self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/unshare", 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
3 weeks ago
@waypoint
@checkmode
def disk_change_storage_policy(
self,
disk_id: int,
storage_policy_id: int,
):
"""
Implementation of functionality of the API method
`/cloudapi/disks/change_disk_storage_policy`.
"""
api_params = {
'disk_id': disk_id,
'storage_policy_id': storage_policy_id,
}
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/disks/change_disk_storage_policy', # noqa: E501
arg_params=api_params,
)
self.set_changed()
return response.json()
##############################
#
# Port Forward rules management
#
##############################
def _pfw_get(self, comp_id, vins_id):
"""Convenience method to get current PFW rules for the specified compute ID.
"""
api_params = dict(vinsId=vins_id)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/natRuleList", api_params)
all_rules = json.loads(api_resp.content.decode('utf8'))
if comp_id == 0:
# no filtering by compute ID, return rules as is
return all_rules
filtered_rules = []
2 years ago
for runner in all_rules['data']:
if runner['vmId'] == comp_id:
filtered_rules.append(runner)
return filtered_rules
def pfw_configure(self, comp_facts, vins_facts, new_rules=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 (list of dicts) new_rules: new PFW rules set. If None is passed, remove all existing
PFW rules for the Compute.
@returns: list of dictionaries with PFW rules as returned by .../vins/natRuleList on success,
None on error.
"""
# At the entry to this method we assume that initial validations are already passed, namely:
# 1) Compute instance exists
# 2) ViNS exists and has GW VNS in valid state
# 3) Compute is connected to this ViNS
#
#
# 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)
#
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "pfw_configure")
ret_rules = []
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'])
return ret_rules
4 years ago
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']:
iface_ipaddr = iface['ipAddress']
break
else:
3 years ago
self.result['failed'] = True
self.result['msg'] = "Compute ID {} is not connected to ViNS ID {}.".format(comp_facts['id'],
4 years ago
vins_facts['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)
if not existing_rules and not new_rules:
self.result['failed'] = False
self.result['warning'] = ("pfw_configure(): both existing and new port forwarding rule lists "
"for Compute ID {} are empty - nothing to do.").format(comp_facts['id'])
return ret_rules
if new_rules == None or len(new_rules) == 0:
# delete all existing rules for this Compute
for rule in existing_rules:
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name="/restmachine/cloudapi/vins/natRuleDel",
arg_params={
'vinsId': vins_facts['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
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
4 years ago
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)
# 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
4 years ago
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?
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'])
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
self.result['failed'] = False
ret_rules = self._pfw_get(comp_facts['id'], vins_facts['id'])
4 years ago
return ret_rules
##############################
#
# K8s management
#
##############################
def k8s_get_by_id(self, k8s_id):
4 years ago
"""Helper function that locates k8s by ID and returns k8s facts.
@param (int) k8s_id: ID of the k8s to find and return facts for.
@return: k8s ID and a dictionary of k8s facts as provided by k8s/get API call. Note that if it fails
4 years ago
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
4 years ago
4 years ago
def k8s_find(self, k8s_id, k8s_name="",rg_id=0,check_state=True):
4 years ago
"""Returns non zero k8s ID and a dictionary with k8s details on success, 0 and empty dictionary otherwise.
This method does not fail the run if k8s cannot be located by its name (arg_k8s_name), because this could be
an indicator of the requested k8s never existed before.
However, it does fail the run if k8s cannot be located by arg_k8s_id (if non zero specified) or if API errors
occur.
@param (int) arg_k8s_id: integer ID of the k8s to be found. If non-zero k8s ID is passed, account ID and k8s name
are ignored. However, k8s must be present in this case, as knowing its ID implies it already exists, otherwise
method will fail.
@param (string) arg_k8s_name: string that defines the name of k8s to be found. This parameter is case sensitive.
@param (bool) arg_check_state: tells the method to report k8s in valid states only.
4 years ago
@return: ID of the k8s, if found. Zero otherwise.
4 years ago
@return: dictionary with k8s facts if k8s is present. Empty dictionary otherwise. None on error.
"""
4 years ago
K8S_INVALID_STATES = ["MODELED","DESTROYED","DESTROYING"]
4 years ago
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_find")
1 year ago
api_params = dict(includedeleted=True)
if k8s_id is None:
api_params.update(name=k8s_name, rgId=rg_id)
else:
api_params.update(by_id=k8s_id)
4 years ago
ret_k8s_id = 0
ret_k8s_dict = None
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/list", api_params)
if api_resp.status_code == 200:
k8s_list = json.loads(api_resp.content.decode('utf8'))
if k8s_list['entryCount'] == 0:
return None,None
for k8s_item in k8s_list['data']:
if not check_state or k8s_item['status'] not in K8S_INVALID_STATES:
ret_k8s_id = k8s_item['id']
ret_k8s_dict = self.k8s_get_by_id(ret_k8s_id)
self.k8s_vins_id = k8s_item['vinsId']
4 years ago
return ret_k8s_id, ret_k8s_dict
def k8s_state(self, arg_k8s_dict, arg_desired_state, arg_started=False):
"""Enable or disable k8s cluster.
@param arg_k8s_dict: dictionary with the target k8s facts as returned by k8s_find(...) method or
.../k8s/get API call.
@param arg_desired_state: the desired state for this k8s cluster. Valid states are 'enabled' and 'disabled'.
@param arg_started: the desired tech state for this k8s cluster. Valid states are 'True' and 'False'.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_state")
NOP_STATES_FOR_K8S_CHANGE = ["MODELED", "DISABLING",
"ENABLING", "DELETING",
"DELETED", "DESTROYING",
"DESTROYED", "CREATING",
"RESTORING"]
VALID_TARGET_STATES = ["ENABLED", "DISABLED"]
5 months ago
4 years ago
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
5 months ago
k8s_state_api = "" # This string will also be used as a flag to indicate that API call is necessary
4 years ago
api_params = dict(k8sId=arg_k8s_dict['id'])
expected_state = ""
tech_state = ""
5 months ago
if arg_k8s_dict['status'] in ['CREATED', 'ENABLED']:
if arg_desired_state == 'disabled':
k8s_state_api = '/restmachine/cloudapi/k8s/disable'
expected_state = 'DISABLED'
elif (
arg_k8s_dict['techStatus'] == 'STARTED'
and arg_desired_state == 'stopped'
):
k8s_state_api = '/restmachine/cloudapi/k8s/stop'
elif (
arg_k8s_dict['techStatus'] == 'STOPPED'
and arg_desired_state == 'started'
):
k8s_state_api = '/restmachine/cloudapi/k8s/start'
elif (
arg_k8s_dict['status'] == 'DISABLED'
and arg_desired_state == 'enabled'
):
k8s_state_api = '/restmachine/cloudapi/k8s/enable'
expected_state = 'ENABLED'
4 years ago
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
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'],
arg_desired_state)
5 months ago
4 years ago
return
4 years ago
4 years ago
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
4 years ago
self.result['msg'] = "k8s_delete() in check mode: delete K8s cluster ID {} was requested.".format(k8s_id)
4 years ago
return
api_params = dict(k8sId=k8s_id,
permanently=permanently,
4 years ago
)
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
4 years ago
self.result['msg'] = "k8s_delete() K8s cluster ID {} was deleted.".format(k8s_id)
4 years ago
self.result['changed'] = True
return
4 years ago
4 years ago
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
4 years ago
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
1 year ago
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',
]:
# 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]
if 'workersGroupName' not in label
]
if (
sorted(filtered_existing_wg_labels)
!= sorted(target_wg[param])
):
target_wg['need_to_recreate'] = True
elif (
target_wg[param] is not None
and existing_wg[param] != target_wg[param]
):
target_wg['need_to_recreate'] = True
if (
target_wg['ci_user_data'] is not None
and not target_wg.get('need_to_recreate')
1 year ago
):
1 year ago
_, vm_info, _ = self._compute_get_by_id(
comp_id=existing_wg['detailedInfo'][0]['id'],
)
if (
vm_info.get('userdata', {})
!= target_wg['ci_user_data']
):
target_wg['need_to_recreate'] = True
1 year ago
3 weeks ago
def k8s_provision(
self,
k8s_name,
k8ci_id,
rg_id,
vins_id,
plugin,
master_count,
master_cpu,
master_ram,
master_disk,
master_sepid,
master_pool,
default_worker,
extnet_id,
with_lb,
ha_lb,
sans,
init_conf,
cluster_conf,
kublet_conf,
kubeproxy_conf,
join_conf,
oidc_cert,
description,
extnet_only,
storage_policy_id: int,
master_chipset: Literal['Q35', 'i440fx'] = 'i440fx',
lb_sysctl: dict | None = None,
zone_id: None | int = None,
):
4 years ago
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_provision")
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = ("k8s_provision() in check mode. Provision k8s '{}' in RG ID {} "
"was requested.").format(k8s_name, rg_id)
return 0
1 year ago
4 years ago
api_url = "/restmachine/cloudapi/k8s/create"
api_params = dict(name=k8s_name,
rgId=rg_id,
k8ciId=k8ci_id,
vinsId=vins_id,
1 year ago
workerGroupName=default_worker['name'],
networkPlugin=plugin,
4 years ago
masterNum=master_count,
masterCpu=master_cpu,
masterRam=master_ram,
masterDisk=master_disk,
masterSepId=master_sepid,
masterSepPool=master_pool,
1 year ago
workerNum=default_worker['num'],
workerCpu=default_worker['cpu'],
workerRam=default_worker['ram'],
workerDisk=default_worker['disk'],
workerSepId=default_worker['sep_id'],
workerSepPool=default_worker['pool'],
labels=default_worker['labels'],
taints=default_worker['taints'],
annotations=default_worker['annotations'],
4 years ago
extnetId=extnet_id,
withLB=with_lb,
highlyAvailableLB=ha_lb,
additionalSANs=sans,
initConfiguration=json.dumps(init_conf) if init_conf else None,
clusterConfiguration=json.dumps(cluster_conf) if cluster_conf else None,
kubeletConfiguration=json.dumps(kublet_conf) if kublet_conf else None,
kubeProxyConfiguration=json.dumps(kubeproxy_conf)if kubeproxy_conf else None,
joinConfiguration=json.dumps(join_conf)if join_conf else None,
1 year ago
desc=description,
userData=json.dumps(default_worker['ci_user_data']),
extnetOnly=extnet_only,
1 year ago
chipset=master_chipset,
9 months ago
lbSysctlParams=lb_sysctl and json.dumps(
{k: str(v) for k, v in lb_sysctl.items()}
),
5 months ago
zoneId=zone_id,
3 weeks ago
storage_policy_id=storage_policy_id,
4 years ago
)
upload_files = None
if oidc_cert:
upload_files = {'oidcCertificate': ('cert.pem', str(oidc_cert),'application/x-x509-ca-cert')}
api_resp = self.decort_api_call(requests.post, api_url, api_params, upload_files)
4 years ago
k8s_id = ""
if api_resp.status_code == 200:
for i in range(300):
api_get_url = "/restmachine/cloudapi/tasks/get"
4 years ago
api_get_params = dict(
auditId=api_resp.content.decode('utf8').replace('"', '')
)
api_get_resp = self.decort_api_call(requests.post, api_get_url, api_get_params)
ret_info = json.loads(api_get_resp.content.decode('utf8'))
if api_get_resp.status_code == 200:
if ret_info['status'] in ["PROCESSING", "SCHEDULED"]:
self.result['msg'] = ("k8s_provision(): Can't create cluster")
4 years ago
self.result['failed'] = False
time.sleep(30)
elif ret_info['status'] == "ERROR":
self.result['msg'] = f"k8s_provision(): {ret_info['error']}"
self.result['changed'] = False
4 years ago
return
elif ret_info['status'] == "OK":
k8s_id = ret_info['result'][0]
self.result['msg'] = f"k8s_provision(): K8s cluster {k8s_name} created successful"
4 years ago
self.result['changed'] = True
return k8s_id
else:
k8s_id = ret_info['status']
else:
self.result['msg'] = ("k8s_provision(): Can't create cluster")
4 years ago
self.result['failed'] = True
# Timeout
self.result['msg'] = ("k8s_provision(): Can't create cluster")
4 years ago
self.result['failed'] = True
else:
self.result['msg'] = ("k8s_provision(): Can't create cluster")
4 years ago
self.result['failed'] = True
self.result['changed'] = False
4 years ago
return
4 years ago
def k8s_workers_modify(self,arg_k8swg,arg_modwg):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_workers_modify")
if self.amodule.check_mode:
result_msg = 'k8s_workers_modify() in check mode: No changing.'
if self.result.get('msg'):
self.result['msg'] += f'\n{result_msg}'
else:
self.result['msg'] = result_msg
return
4 years ago
if self.k8s_info['techStatus'] != "STARTED":
self.result['msg'] = ("k8s_workers_modify(): Can't modify with TechStatus other then STARTED")
return
1 year ago
4 years ago
wg_del_list = []
wg_add_list = []
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']]
for rec in arg_k8swg['k8sGroups']['workers']:
if rec['name'] not in wg_outer:
wg_del_list.append(rec['id'])
1 year ago
4 years ago
for rec in arg_modwg:
if rec['name'] not in wg_inner:
wg_add_list.append(rec)
1 year ago
1 year ago
for wg in arg_k8swg['k8sGroups']['workers']:
for target_wg in arg_modwg:
if wg['name'] == target_wg['name']:
self.k8s_check_worker_group_for_recreate(
target_wg=target_wg,
existing_wg=wg,
)
if target_wg.get('need_to_recreate'):
wg_del_list.append(wg['id'])
wg_to_create = deepcopy(target_wg)
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'],
)
wg_to_create[param] = vm_info.get(
'userdata', {}
)
elif value is None:
wg_to_create[param] = wg.get(param)
wg_add_list.append(wg_to_create)
continue
w_ids = {w['id'] for w in wg['detailedInfo']}
bad_w_ids = set()
new_chipset = target_wg['chipset']
if new_chipset is not None:
for w_id in w_ids:
_, vm_info, _ = self._compute_get_by_id(
comp_id=w_id,
)
if vm_info['chipset'] != new_chipset:
bad_w_ids.add(w_id)
wg_num = wg['num']
target_num = target_wg['num']
if target_num is not None:
new_w_count = target_num - wg_num + len(bad_w_ids)
if new_w_count > 0:
if new_chipset is None:
new_chipset = self.wg_default_params['chipset']
wg_modadd_list.append({
'wg_id': wg['id'],
'computes_num': new_w_count,
'chipset': new_chipset,
})
elif new_w_count:
valid_w_ids = w_ids.difference(bad_w_ids)
for _ in range(abs(new_w_count)):
bad_w_ids.add(valid_w_ids.pop())
if bad_w_ids:
wg_moddel_list.append({
'wg_id': wg['id'],
'compute_ids': bad_w_ids,
})
4 years ago
if wg_del_list:
for wgid in wg_del_list:
api_params = dict(k8sId=self.k8s_id,workersGroupId=wgid)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/workersGroupDelete", api_params)
self.result['changed'] = True
if wg_add_list:
for wg in wg_add_list:
1 year ago
wg_to_create = deepcopy(wg)
for param, default_value in self.wg_default_params.items():
if wg_to_create[param] is None:
wg_to_create[param] = default_value
1 year ago
api_params = {
'k8sId': self.k8s_id,
1 year ago
'name': wg_to_create['name'],
'workerNum': wg_to_create['num'],
'workerCpu': wg_to_create['cpu'],
'workerRam': wg_to_create['ram'],
'workerDisk': wg_to_create['disk'],
'workerSepId': wg_to_create['sep_id'],
'workerSepPool': wg_to_create['pool'],
'labels': wg_to_create['labels'],
'taints': wg_to_create['taints'],
'annotations': wg_to_create['annotations'],
'userData': json.dumps(wg_to_create['ci_user_data']),
'chipset': wg_to_create['chipset'],
1 year ago
}
wg_add_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/k8s/workersGroupAdd',
arg_params=api_params,
)
self.set_changed()
audit_id = wg_add_response.text.strip('"')
task_link = (
f'{self.controller_url}/portal/#/system/tasks/{audit_id}'
)
params = {'auditId': audit_id}
# average time to add a single worker group * reserve
wg_add_avg_time = 210 * 2
1 year ago
wg_add_timeout = wg_add_avg_time * wg_to_create['num']
1 year ago
task_schedule_timeout = 600
sleep_interval = 5
while True:
task_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/tasks/get',
arg_params=params,
)
response_data = task_response.json()
match response_data['status']:
case 'SCHEDULED':
if task_schedule_timeout <= 0:
self.message(
1 year ago
f'Time to schedule task to add worker '
f'group {wg_to_create["name"]} has been '
f'exceeded.'
1 year ago
f'\nTask details: {task_link}'
4 years ago
)
1 year ago
self.exit(fail=True)
time.sleep(sleep_interval)
task_schedule_timeout -= sleep_interval
case 'PROCESSING':
if wg_add_timeout <= 0:
self.message(
1 year ago
f'Time to add worker group '
f'{wg_to_create["name"]} has been '
f'exceeded.\nTask details: {task_link}'
1 year ago
)
self.exit(fail=True)
time.sleep(sleep_interval)
wg_add_timeout -= sleep_interval
case 'ERROR':
self.result['msg'] = (
1 year ago
f'Adding worker group {wg_to_create["name"]} '
f'failed: {response_data["error"]}.'
1 year ago
f'\nTask details: {task_link}'
)
self.exit(fail=True)
case 'OK':
self.message(
1 year ago
f'Worker group {wg_to_create["name"]} '
f'created successful'
1 year ago
)
break
4 years ago
if wg_modadd_list:
for wg in wg_modadd_list:
1 year ago
api_params = {
'k8sId': self.k8s_id,
'workersGroupId': wg['wg_id'],
'num': wg['computes_num'],
'chipset': wg['chipset'],
}
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/k8s/workerAdd',
arg_params=api_params,
)
self.result['changed'] = True
4 years ago
if wg_moddel_list:
for wg in wg_moddel_list:
1 year ago
for compute_id in wg['compute_ids']:
api_params = {
'k8sId': self.k8s_id,
'workersGroupId': wg['wg_id'],
'workerId': compute_id,
}
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/k8s/deleteWorkerFromGroup',
arg_params=api_params,
)
self.result['changed'] = True
4 years ago
self.result['failed'] = False
return
3 weeks ago
@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
4 years ago
def k8s_k8ci_find(self,arg_k8ci_id):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_k8ci_find")
api_params = dict(includeDisabled=False)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8ci/list", api_params)
k8ci_id_present = False
4 years ago
if api_resp.status_code == 200:
ret_k8ci_list = json.loads(api_resp.content.decode('utf8'))
for k8ci_item in ret_k8ci_list['data']:
4 years ago
if k8ci_item['id'] == arg_k8ci_id:
k8ci_id_present = True
4 years ago
break
else:
self.result['failed'] = True
self.result['msg'] = ("Cannot find k8ci id: {}.").format(arg_k8ci_id)
self.amodule.fail_json(**self.result)
4 years ago
else:
self.result['failed'] = True
self.result['msg'] = ("Failed to get k8ci list HTTP code {}.").format(api_resp.status_code)
self.amodule.fail_json(**self.result)
return arg_k8ci_id
def k8s_getConfig(self):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_getConfig")
api_params = dict(k8sId=self.k8s_id)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/getConfig", api_params)
ret_conf = api_resp.content.decode('utf8')
return ret_conf
5 months ago
@waypoint
@checkmode
def k8s_migrate_to_zone(self, k8s_id: int, zone_id: int):
api_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/k8s/migrateToZone',
arg_params={
'k8sId': k8s_id,
'zoneId': zone_id,
},
)
self.set_changed()
return api_response.json()
##############################
#
# Bservice management
#
##############################
def bservice_get_by_id(self,bs_id):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "bservice_get_by_id")
ret_bs_id = 0
ret_bs_dict = dict()
if not bs_id:
self.result['failed'] = True
self.result['msg'] = "bservice_get_by_id(): zero B-Service ID specified."
self.amodule.fail_json(**self.result)
api_params = dict(serviceId=bs_id, )
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/bservice/get", api_params)
if api_resp.status_code == 200:
ret_bs_id = bs_id
ret_bs_dict = json.loads(api_resp.content.decode('utf8'))
else:
self.result['warning'] = ("bservice_get_by_id(): failed to get B-service by ID {}. HTTP code {}, "
"response {}.").format(bs_id, api_resp.status_code, api_resp.reason)
return ret_bs_id, ret_bs_dict
def _bservice_rg_list(self,acc_id,rg_id):
ret_bs_dict=dict()
api_params = dict(accountId=acc_id,rgId=rg_id )
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/bservice/list", api_params)
if api_resp.status_code == 200:
ret_bs_dict = json.loads(api_resp.content.decode('utf8'))
else:
self.result['warning'] = ("bservice_rg_list(): failed to get B-service list. HTTP code {}, "
"response {}.").format(api_resp.status_code, api_resp.reason)
return []
return ret_bs_dict['data']
def bservice_find(self,account_id,rg_id,bservice_name="",bservice_id = 0,check_state=True):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "bservice_find")
if bservice_id == 0:
bs_rg_list = self._bservice_rg_list(account_id,rg_id)
for srv in bs_rg_list:
if bservice_name == srv['name']:
bservice_id = int(srv['id'])
if bservice_id > 0:
ret_bs_id,ret_bs_dict = self.bservice_get_by_id(bservice_id)
return ret_bs_id,ret_bs_dict
else:
return bservice_id,None
5 months ago
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,
5 months ago
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
5 months ago
def bservice_state(self,bs_dict,desired_state):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "bservice_state")
3 weeks ago
NOP_STATES_FOR_BS_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING",
"DESTROYED","RESTORYNG","RECONFIGURING"]
3 weeks ago
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
3 weeks ago
if (
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 = ""
3 weeks ago
if bs_dict['status'] == 'CREATED':
if desired_state == 'disabled':
bsstate_api = '/restmachine/cloudapi/bservice/disable'
elif desired_state == 'enabled':
bsstate_api = '/restmachine/cloudapi/bservice/enable'
if bs_dict['status'] == 'ENABLED':
5 months ago
if desired_state == 'disabled':
bsstate_api = '/restmachine/cloudapi/bservice/disable'
expected_state = 'DISABLED'
elif (
bs_dict['techStatus'] == 'STARTED'
and desired_state == 'stopped'
):
bsstate_api = '/restmachine/cloudapi/bservice/stop'
elif (
3 weeks ago
bs_dict['techStatus'] == 'STOPPED'
5 months ago
and desired_state == 'started'
):
bsstate_api = '/restmachine/cloudapi/bservice/start'
elif (
bs_dict['status'] == 'DISABLED'
and desired_state == 'enabled'
):
bsstate_api = '/restmachine/cloudapi/bservice/enable'
expected_state = 'ENABLED'
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
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'],
desired_state)
5 months ago
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
5 months ago
@waypoint
@checkmode
def bservice_migrate_to_zone(self, bs_id: int, zone_id: int):
api_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/bservice/migrateToZone',
arg_params={
'serviceId': bs_id,
'zoneId': zone_id,
},
)
self.set_changed()
return api_response.json()
#
# GROUP MANAGE
#
def _group_get_by_id(self,bs_id,g_id):
3 weeks ago
ret_gr_dict = {}
api_params = dict(serviceId=bs_id,compgroupId=g_id)
3 weeks ago
api_resp = self.decort_api_call(
requests.post,
'/restmachine/cloudapi/bservice/groupGet',
api_params,
not_fail_codes=[400],
)
if api_resp.status_code == 200:
ret_gr_dict = json.loads(api_resp.content.decode('utf8'))
3 weeks ago
elif api_resp.status_code == 400:
self.message(
f'Group with ID {g_id} for BService with ID {bs_id} not found '
f'or Group with ID {g_id} wast deleted.'
)
self.exit()
else:
3 weeks ago
self.result['warning'] = (
f'group_get_by_id(): failed to get Group by ID {g_id}. '
f'HTTP code {api_resp.status_code}, response {api_resp.reason}.'
)
return g_id, ret_gr_dict
1 year ago
def group_find(self,bs_id,bs_info,group_id=None,group_name=""):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_find")
1 year ago
if not group_id:
for group in bs_info['groups']:
if group['name'] == group_name:
return self._group_get_by_id(bs_id=bs_id,
g_id=group['id'])
return 0, None
return self._group_get_by_id(bs_id,group_id)
def group_state(self,bs_id,gr_id,desired_state):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_state")
group_api=""
if desired_state == 'stopped':
group_api = "/restmachine/cloudapi/bservice/groupStop"
state_expected = "STOPPED"
else:
group_api = "/restmachine/cloudapi/bservice/groupStart"
state_expected = "STARTED"
api_params = dict(
serviceId=bs_id,
compgroupId=gr_id
)
if group_api != "":
self.decort_api_call(requests.post, group_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
else:
self.result['failed'] = False
self.result['msg'] = ("group_state(): no start/stop action required for B-service ID {} "
"to desired state '{}'.").format(bs_id,desired_state)
return
7 months ago
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")
count = len(gr_dict['computes'])
if desired_count != count:
api_params=dict(
serviceId=bs_id,
compgroupId=gr_dict['id'],
count=desired_count,
7 months ago
mode="ABSOLUTE",
chipset=chipset,
)
api_url = "/restmachine/cloudapi/bservice/groupResize"
self.decort_api_call(requests.post, api_url, api_params)
self.result['failed'] = False
self.result['changed'] = True
else:
self.result['failed'] = False
self.result['msg'] = ("group_resize_count(): no need resize Group ID {}.").format(gr_dict['id'])
return
1 year ago
@waypoint
def group_update(self,bs_id,gr_dict,arg_cpu,arg_disk,arg_name,arg_role,arg_ram):
api_params=dict(
serviceId=bs_id,
compgroupId=gr_dict['id'],
force=True,
1 year ago
cpu=arg_cpu,
ram=arg_ram,
role=arg_role,
disk=arg_disk,
name=arg_name,
)
1 year ago
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/bservice/groupUpdate',
arg_params=api_params,
)
self.set_changed()
def group_update_net(self,bs_id,gr_dict,arg_net):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_update_net")
list_vins= list()
list_extnet= list()
for net in arg_net:
if net['type'] == 'VINS':
list_vins.append(net['id'])
else:
list_extnet.append(net['id'])
3 weeks ago
if len(list_vins) > 0:
if sorted(gr_dict['vinses']) != sorted(list_vins):
api_url = "/restmachine/cloudapi/bservice/groupUpdateVins"
api_params = dict(
serviceId=bs_id,
compgroupId=gr_dict['id'],
vinses=list_vins
)
self.decort_api_call(requests.post, api_url, api_params)
self.set_changed()
3 weeks ago
if len(list_extnet) > 0:
if sorted(gr_dict['extnets']) != sorted(list_extnet):
api_url = '/restmachine/cloudapi/bservice/groupUpdateExtnet'
api_params = dict(
serviceId=bs_id,
compgroupId=gr_dict['id'],
extnets=list_extnet
)
self.decort_api_call(requests.post, api_url, api_params)
self.set_changed()
return
def group_provision(
3 weeks ago
self,
bs_id,
arg_name,
chipset: Literal['Q35', 'i440fx'],
storage_policy_id: int,
driver: str,
arg_count=1,
arg_cpu=1,
arg_ram=1024,
arg_boot_disk=10,
arg_image_id=0,
arg_role="",
arg_network=None,
arg_timeout=0,
7 months ago
):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_provision")
1 year ago
arg_network = arg_network or []
api_url = "/restmachine/cloudapi/bservice/groupAdd"
api_params = dict(
serviceId = bs_id,
name = arg_name,
count = arg_count,
cpu = arg_cpu,
ram = arg_ram,
disk = arg_boot_disk,
imageId = arg_image_id,
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'],
7 months ago
timeoutStart = arg_timeout,
chipset=chipset,
3 weeks ago
storage_policy_id=storage_policy_id,
driver=driver,
)
api_resp = self.decort_api_call(requests.post, api_url, api_params)
new_bsgroup_id = int(api_resp.text)
self.result['failed'] = False
self.result['changed'] = True
return new_bsgroup_id
def group_delete(self,bs_id,gr_id):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_delete")
api_url = "/restmachine/cloudapi/bservice/groupRemove"
api_params=dict(
serviceId = bs_id,
compgroupId = gr_id
)
self.decort_api_call(requests.post, api_url, api_params)
self.result['failed'] = False
self.result['msg'] = "group_delete() Group ID {} was deleted.".format(gr_id)
self.result['changed'] = True
return
####################
### LB MANAGMENT ###
####################
def _lb_get_by_id(self,lb_id):
"""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.
@return: LB ID and a dictionary of LB facts as provided by LB/get API call.
"""
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)
api_params = dict(lbId=lb_id)
3 weeks ago
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'))
3 weeks ago
elif api_resp.status_code == 404:
self.message(f'LB with ID {lb_id} not found.')
self.exit(fail=True)
else:
3 weeks ago
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}.'
)
self.exit(fail=True)
return ret_lb_id, ret_lb_dict
3 weeks ago
def _rg_listlb(self,rg_id):
"""List all LB in the resource group
@param (int) rg_id: id onr resource group
"""
if not rg_id:
self.result['failed'] = True
self.result['msg'] = "_rg_listlb(): zero RG ID specified."
self.amodule.fail_json(**self.result)
3 weeks ago
api_params = dict(includedeleted=True)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/list", api_params)
if api_resp.status_code == 200:
ret_rg_vins_list = json.loads(api_resp.content.decode('utf8'))
else:
self.result['warning'] = ("rg_listlb(): failed to get RG by ID {}. HTTP code {}, "
"response {}.").format(rg_id, api_resp.status_code, api_resp.reason)
return []
return ret_rg_vins_list['data']
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"]
3 weeks ago
api_params = dict()
ret_lb_id = 0
ret_lb_facts = None
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)
3 weeks ago
if not self.amodule.check_mode or ret_lb_facts['status'] not in LB_INVALID_STATES:
return ret_lb_id, ret_lb_facts
else:
return 0, None
elif lb_name != "":
if rg_id > 0:
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
else:
return 0, None
else:
self.result['failed'] = True
self.result['msg'] = ("vins_lb(): cannot find LB by name '{}' "
"when no account ID or RG ID is specified.").format(lb_name)
self.amodule.fail_json(**self.result)
else:
self.result['failed'] = True
self.result['msg'] = "vins_find(): cannot find LB by zero ID and empty name."
self.amodule.fail_json(**self.result)
return 0, None
5 months ago
def lb_provision(
self,
lb_name,
rg_id,
vins_id,
ext_net_id,
ha_status,
description,
start=True,
sysctl: dict | None = None,
zone_id: None | int = None,
):
"""Provision LB according to the specified arguments.
If critical error occurs the embedded call to API function will abort further execution of
the script and relay error to Ansible.
Note, that when creating ViNS at account level, default location under DECORT controller
will be selected automatically.
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_provision")
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = ("vins_lb() in check mode: provision LB name '{}' was "
"requested in RG with id: {}.").format(lb_name,rg_id)
return 0
if lb_name == "":
self.result['failed'] = True
self.result['msg'] = "lb_provision(): LB name cannot be empty."
self.amodule.fail_json(**self.result)
api_url = "/restmachine/cloudapi/lb/create"
api_params = dict(
name=lb_name,
rgId=rg_id,
extnetId=ext_net_id,
vinsId=vins_id,
highlyAvailable=ha_status,
start=start,
9 months ago
desc=description,
sysctlParams=sysctl and json.dumps(
{k: str(v) for k, v in sysctl.items()}
),
5 months ago
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.
self.result['failed'] = False
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
3 weeks ago
return lb_id
def lb_state(self, lb_dict, desired_state):
"""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"]
5 months ago
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'])
return
5 months ago
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'])
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"]:
if desired_state == 'disabled':
state_api = "/restmachine/cloudapi/lb/disable"
expected_state = "DISABLED"
if lb_dict['techStatus'] == "STARTED":
if desired_state == 'stopped':
state_api = "/restmachine/cloudapi/lb/stop"
if desired_state == 'restart':
state_api = "/restmachine/cloudapi/lb/restart"
elif lb_dict['techStatus'] == "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"
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
5 months ago
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)
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):
"""
backends
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_update_backends")
if mod_backends:
backs_out = [rec['name'] for rec in mod_backends]
else:
backs_out=""
backs_in = [rec['name'] for rec in lb_backends]
del_backs = set(backs_in).difference(backs_out)
add_becks = set(backs_out).difference(backs_in)
upd_becks = set(backs_in).intersection(backs_out)
for item in del_backs:
#need delete frontend
api_params = dict(lbId=self.lb_id,backendName = item)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendDelete", api_params)
self.result['changed'] = True
for item in add_becks:
backend, = list(filter(lambda i: i['name'] == item,mod_backends))
api_params = dict(
lbId=self.lb_id,
backendName = backend['name'],
algorithm = backend['algorithm'] if "algorithm" in backend else None,
**backend['default_settings'] if "default_settings" in backend else {},
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendCreate", api_params)
self.result['changed'] = True
for item in upd_becks.union(add_becks):
backend, = list(filter(lambda i: i['name'] == item,lb_backends))
mod_backend, = list(filter(lambda i: i['name'] == item,mod_backends))
servers_in = [rec['name'] for rec in backend['servers']]
servers_out = []
#oO rework
if mod_servers:
for serv in mod_servers:
for bend in serv['backends']:
if bend['name'] == item:
servers_out.append(serv['name'])
del_srv = set(servers_in).difference(servers_out)
add_srv = set(servers_out).difference(servers_in)
upd_srv = set(servers_in).intersection(servers_out)
del backend['serverDefaultSettings']['guid']
if "default_settings" not in mod_backend:
mod_backend["default_settings"] = self.default_settings
if "algorithm" not in mod_backend:
mod_backend["algorithm"] = self.default_alg
if backend['serverDefaultSettings'] != mod_backend["default_settings"] or\
mod_backend["algorithm"] != backend['algorithm']:
api_params = dict(
lbId=self.lb_id,
backendName = item,
algorithm = mod_backend['algorithm'],
**mod_backend['default_settings'] if "default_settings" in mod_backend else {},
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendUpdate", api_params)
self.result['changed'] = True
for srv in del_srv:
api_params = dict(lbId=self.lb_id,backendName = item,serverName=srv)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendServerDelete", api_params)
self.result['changed'] = True
for srv in add_srv:
server, = list(filter(lambda i: i['name'] == srv,mod_servers))
back, = list(filter(lambda i: i['name'] == item,server['backends']))
api_params = dict(
lbId=self.lb_id,
backendName = item,
serverName = server['name'],
address = server['address'],
port = back['port'],
check = server['check'] if "check" in server else None,
**server['server_settings'] if "server_settings" in server else {},
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendServerAdd", api_params)
self.result['changed'] = True
for srv in upd_srv:
mod_server, = list(filter(lambda i: i['name'] == srv,mod_servers))
mod_server_back, = list(filter(lambda i: i['name'] == item,mod_server['backends']))
server, = list(filter(lambda i: i['name'] == srv, backend['servers']))
del server['serverSettings']['guid']
if "server_settings" not in mod_server_back:
mod_server_back['server_settings'] = self.default_settings
if "check" not in mod_server:
mod_server['check'] = self.default_server_check
if (mod_server['address'] != server['address'] or\
server['check']!=mod_server["check"]) or\
mod_server_back['server_settings'] != server['serverSettings']:
api_params = dict(
lbId=self.lb_id,
backendName = item,
serverName = mod_server['name'],
address = mod_server['address'],
port = mod_server_back['port'],
check = mod_server_back['check'] if "check" in mod_server_back else None,
**mod_server_back['server_settings'] if "server_settings" in mod_server_back else {},
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendServerUpdate", api_params)
self.result['changed'] = True
return
def lb_update_frontends(self,lb_frontends,mod_frontends):
"""
frontends
"""
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_update_frontends")
if mod_frontends:
front_out = [rec['name'] for rec in mod_frontends]
else:
front_out=""
front_in = [rec['name'] for rec in lb_frontends]
del_front = set(front_in).difference(front_out)
add_front = set(front_out).difference(front_in)
upd_front = set(front_in).intersection(front_out)
for front in del_front:
delete_front, = list(filter(lambda i: i['name'] == front,lb_frontends))
api_params = dict(
lbId=self.lb_id,
frontendName=delete_front['name'],
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/frontendDelete", api_params)
self.result['changed'] = True
for front in add_front:
create_front, = list(filter(lambda i: i['name'] == front,mod_frontends))
api_params = dict(
lbId=self.lb_id,
frontendName=create_front['name'],
backendName=create_front['backend'],
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/frontendCreate", api_params)
self.result['changed'] = True
if "bindings" in create_front:
for bind in create_front['bindings']:
self._lb_bind_frontend(
create_front['name'],
bind['name'],
bind['address'] if "address" in bind else None,
bind['port'] if "port" in bind else None
)
for front in upd_front:
update_front, = list(filter(lambda i: i['name'] == front,mod_frontends))
lb_front, = list(filter(lambda i: i['name'] == front,lb_frontends))
mod_bind = [rec['name'] for rec in update_front['bindings']]
lb_bind = [rec['name'] for rec in lb_front['bindings']]
del_bind_list = set(lb_bind).difference(mod_bind)
add_bind_list = set(mod_bind).difference(lb_bind)
upd_bind_list = set(lb_bind).intersection(mod_bind)
for bind_name in del_bind_list:
del_bind, = list(filter(lambda i: i['name'] == bind_name,lb_front['bindings']))
api_params = dict(
lbId=self.lb_id,
frontendName=update_front['name'],
bindingName=del_bind['name'],
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/frontendBindDelete", api_params)
self.result['changed'] = True
for bind_name in add_bind_list:
add_bind, = list(filter(lambda i: i['name'] == bind_name,update_front['bindings']))
self._lb_bind_frontend(
update_front['name'],
add_bind['name'],
add_bind['address'] if "address" in add_bind else None,
add_bind['port'] if "port" in add_bind else None
)
for bind_name in upd_bind_list:
lb_act_bind, = list(filter(lambda i: i['name'] == bind_name,lb_front['bindings']))
mod_act_bind, = list(filter(lambda i: i['name'] == bind_name,update_front['bindings']))
del lb_act_bind['guid']
if lb_act_bind != mod_act_bind:
self._lb_bind_frontend(
update_front['name'],
mod_act_bind['name'],
mod_act_bind['address'] if "address" in mod_act_bind else None,
mod_act_bind['port'] if "port" in mod_act_bind else None,
update=True
)
return
def _lb_bind_frontend(self,front_name,bind_name,bind_addr=None,bind_port=None,update=False):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_bind_frontend")
api_params = dict(
lbId=self.lb_id,
frontendName=front_name,
bindingName=bind_name,
bindingAddress=bind_addr,
bindingPort=bind_port,
)
if update == True:
api_url = "/restmachine/cloudapi/lb/frontendBindingUpdate"
else:
api_url = "/restmachine/cloudapi/lb/frontendBind"
api_resp = self.decort_api_call(requests.post, api_url, api_params)
self.result['changed'] = True
9 months ago
def lb_update(
self,
lb_facts: dict,
aparam_backends: list | None,
aparam_frontends: list | None,
aparam_servers: list | None,
5 months ago
aparam_sysctl: dict | None = None,
9 months ago
):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_update")
if self.amodule.check_mode:
result_msg = 'lb_update() in check mode: No changing.'
if self.result.get('msg'):
self.result['msg'] += f'\n{result_msg}'
else:
self.result['msg'] = result_msg
return
9 months ago
lb_backends = lb_facts['backends']
lb_frontends = lb_facts['frontends']
if aparam_backends is None:
1 year ago
upd_back_list = [back['name'] for back in lb_backends]
else:
#lists from module and cloud
9 months ago
mod_backs_list = [back['name'] for back in aparam_backends]
1 year ago
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)
upd_back_list = set(lb_backs_list).intersection(mod_backs_list)
1 year ago
#FE
1 year ago
if del_list_backs:
self._lb_delete_backends(
del_list_backs,
lb_frontends
)
if add_back_list:
self._lb_create_backends(
add_back_list,
9 months ago
aparam_backends,
aparam_servers
1 year ago
)
if upd_back_list:
9 months ago
if aparam_backends is not None or aparam_servers is not None:
1 year ago
self._lb_update_backends(
upd_back_list,
lb_backends,
9 months ago
aparam_backends,
aparam_servers,
1 year ago
)
9 months ago
if aparam_frontends is not None:
mod_front_list = [front['name'] for front in aparam_frontends]
1 year ago
lb_front_list = [front['name'] for front in lb_frontends]
1 year ago
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)
1 year ago
if del_list_fronts:
self._lb_delete_fronts(del_list_fronts)
9 months ago
front_ha_ip = lb_facts['frontendHAIP']
back_ha_ip = lb_facts['backendHAIP']
1 year ago
#set bind_ip
if front_ha_ip != "":
bind_ip = front_ha_ip
if front_ha_ip == "" and back_ha_ip != "":
bind_ip = back_ha_ip
if front_ha_ip == "" and back_ha_ip == "":
9 months ago
prime = lb_facts['primaryNode']
1 year ago
if prime["frontendIp"] != "":
bind_ip = prime["frontendIp"]
else:
bind_ip = prime["backendIp"]
1 year ago
if add_list_fronts:
9 months ago
self._lb_add_fronts(add_list_fronts,aparam_frontends,bind_ip)
1 year ago
if upd_front_list:
9 months ago
self._lb_update_fronts(upd_front_list,lb_frontends,aparam_frontends,bind_ip)
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']:
self.lb_update_sysctl(
lb_id=lb_facts['id'],
sysctl=aparam_sysctl,
)
return
def _lb_delete_backends(self,back_list,lb_fronts):
#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))
if fronts:
self._lb_delete_fronts(fronts)
api_params = dict(
lbId=self.lb_id,
backendName = back
)
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):
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,
)
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):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_add_fronts")
for front in front_list:
add_front, = list(filter(lambda i: i['name'] == front,mod_fronts))
api_params = dict(
lbId=self.lb_id,
frontendName=add_front['name'],
backendName=add_front['backend'],
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/frontendCreate", api_params)
for bind in add_front['bindings']:
self._lb_bind_frontend(
add_front['name'],
bind['name'],
bind['address']if "address" in bind else bind_ip,
bind['port'],
)
return
def _lb_create_backends(self,back_list,mod_backs,mod_serv):
'''
Create backends and add servers to them
'''
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_create_backends")
for back in back_list:
backend, = list(filter(lambda i: i['name'] == back,mod_backs))
api_params = dict(
lbId=self.lb_id,
backendName = back,
algorithm = backend['algorithm'] if "algorithm" in backend else None,
**backend['default_settings'] if "default_settings" in backend else {},
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendCreate", api_params)
for server in mod_serv:
try:
srv_back, = list(filter(lambda i: i['name'] == back,server['backends']))
except:
continue
api_params = dict(
lbId=self.lb_id,
backendName = back,
serverName = server['name'],
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 {},
)
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):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "lb_update_backends")
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))
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)
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']:
api_params = dict(
lbId=self.lb_id,
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']]
for server in mod_serv:
try:
mod_back, = list(filter(lambda i: i['name'] == back['name'],server['backends']))
except:
continue
if server['name'] not in lb_servers_list:
api_params = dict(
lbId=self.lb_id,
backendName = mod_back['name'],
serverName = server['name'],
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 {},
)
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']
if "server_settings" not in mod_back:
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)
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']:
api_params = dict(
lbId=self.lb_id,
backendName = mod_back['name'],
serverName = server['name'],
address = server['address'],
port = mod_back['port'],
check = mod_back['check'],
**mod_back['server_settings'],
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendServerUpdate", api_params)
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_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/backendServerDelete", api_params)
self.result['changed'] = True
return
def _lb_update_fronts(self,upd_front_list,lb_frontends,mod_frontends,bind_ip):
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']]
for bind in mod_front['bindings']:
if bind['name'] not in lb_binds_list:
pass
self._lb_bind_frontend(
front,
bind['name'],
bind['address']if "address" in bind else bind_ip,
bind['port'],
)
else:
lb_bind, = list(filter(lambda i: i['name'] == bind['name'],lb_front['bindings']))
del lb_bind['guid']
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['port'],
update=True,
)
lb_binds_list.remove(bind['name'])
for lb_bind in lb_binds_list:
api_params = dict(
lbId=self.lb_id,
frontendName=front,
bindingName=lb_bind,
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/lb/frontendBindDelete", api_params)
self.result['changed'] = True
return
12 months ago
9 months ago
@waypoint
@checkmode
def lb_update_sysctl(self, lb_id: int, sysctl: dict):
sysctl_with_str_values = json.dumps(
{k: str(v) for k, v in sysctl.items()}
)
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/lb/updateSysctlParams',
arg_params={
'lbId': lb_id,
'sysctlParams': sysctl_with_str_values,
},
)
self.set_changed()
5 months ago
@waypoint
@checkmode
def lb_migrate_to_zone(self, lb_id: int, zone_id: int):
api_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/lb/migrateToZone',
arg_params={
'lbId': lb_id,
'zoneId': zone_id,
},
)
self.set_changed()
return api_response.json()
12 months ago
@waypoint
@checkmode
def snapshot_create(self, compute_id: int, label: str):
"""
Implementation of functionality of the API method
`/cloudapi/compute/snapshotCreate`.
"""
self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/snapshotCreate',
arg_params={
'computeId': compute_id,
'label': label,
},
)
self.set_changed()
self.message(
f'Snapshot {label} for VM ID {compute_id} created successfully.'
)
@waypoint
@checkmode
def snapshot_delete(
self,
compute_id: int,
label: str,
):
"""
Implementation of functionality of the API method
`/cloudapi/compute/snapshotDelete`.
The method `self.exit(fail=True)` will be
called if the time for scheduling or for deleting a snapshot
is exceeded, or if an error occurs during deletion.
"""
snapshot_delete_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/snapshotDelete',
arg_params={
'computeId': compute_id,
'label': label,
'asyncMode': True,
},
)
self.set_changed()
audit_id = snapshot_delete_response.text.strip('"')
task_link = (
f'{self.controller_url}/portal/#/system/tasks/{audit_id}'
)
params = {'auditId': audit_id}
task_schedule_timeout = 600
snapshot_delete_timeout = 120
sleep_interval = 5
while True:
task_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/tasks/get',
arg_params=params,
)
response_data = task_response.json()
match response_data['status']:
case 'SCHEDULED':
if task_schedule_timeout <= 0:
self.message(
f'Time to schedule task to delete snapshot for '
f'VM ID {compute_id} has been exceeded.'
f'\nTask details: {task_link}'
)
self.exit(fail=True)
task_schedule_timeout -= sleep_interval
case 'PROCESSING':
if snapshot_delete_timeout <= 0:
self.message(
f'Time to delete snapshot for VM ID {compute_id} '
f'has been exceeded.'
f'\nTask details: {task_link}'
)
self.exit(fail=True)
snapshot_delete_timeout -= sleep_interval
case 'ERROR':
self.result['msg'] = (
f'Deleting snapshot for VM ID {compute_id} failed:'
f'{response_data["error"]}.'
f'\nTask details: {task_link}'
)
self.exit(fail=True)
case 'OK':
self.message(
f'Snapshot {label} for VM ID {compute_id} '
f'deleted successfully.'
)
break
time.sleep(sleep_interval)
@waypoint
def snapshot_list(self, compute_id: int) -> list:
"""
Implementation of functionality of the API method
`/cloudapi/compute/snapshotList`.
"""
snapshots_list_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/snapshotList',
arg_params={
'computeId': compute_id,
}
)
return snapshots_list_response.json()['data']
@waypoint
def snapshot_usage(
self,
compute_id: int,
label: Optional[str],
) -> tuple[dict, list[dict]]:
"""
Implementation of functionality of the API method
`/cloudapi/compute/snapshotUsage`.
"""
snapshot_usage_response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/snapshotUsage',
arg_params={
'computeId': compute_id,
'label': label,
}
)
snapshot_usage = snapshot_usage_response.json()
common_snapshot_usage_info = snapshot_usage[0]
snapshots_usage = snapshot_usage[1:]
return common_snapshot_usage_info, snapshots_usage
5 months ago
@waypoint
@checkmode
def snapshot_abort_merge(self, vm_id: int, label: str) -> str:
response = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/compute/abort_shared_snapshot_merge', # noqa: E501
arg_params={
'compute_id': vm_id,
'label': label,
},
)
self.set_changed()
self.message(
f'Merge aborted for snapshot {label} of VM ID {vm_id}.'
)
return response.text
def check_aparam_zone_id(self) -> bool | None:
aparam_zone_id = self.aparams['zone_id']
if aparam_zone_id is not None:
if aparam_zone_id in self.acc_zone_ids:
return True
else:
self.message(
'Check for parameter "zone_id" failed: '
f'zone ID {aparam_zone_id} not available for account ID {self.acc_id}.'
)
return False
@waypoint
def zone_get(self, id: int) -> dict:
"""
Implementation of functionality of the API method
`/cloudapi/zone/get`.
"""
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/zone/get',
arg_params={
'id': id,
},
not_fail_codes=[403, 404],
)
zone_info = None
if api_resp.status_code == 200:
zone_info = api_resp.json()
if not zone_info:
self.message(
self.MESSAGES.obj_not_found(obj='zone', id=id)
)
self.exit(fail=True)
return zone_info
@waypoint
def trunk_get(self, id: int) -> dict:
"""
Implementation of functionality of the API method
`/cloudapi/trunk/get`.
"""
api_resp = self.decort_api_call(
arg_req_function=requests.get,
arg_api_name='/restmachine/cloudapi/trunk/get',
arg_params={
'id': id,
},
not_fail_codes=[404],
)
trunk_info = None
if api_resp.status_code == 200:
trunk_info = api_resp.json()
if not trunk_info:
self.message(
self.MESSAGES.obj_not_found(obj='trunk', id=id)
)
self.exit(fail=True)
return trunk_info
@waypoint
def user_trunks(
self,
account_ids: None | list = None,
ids: None | list = None,
status: None | TrunkStatus = None,
vlan_ids: None | list = None,
page_number: int = 1,
page_size: None | int = None,
sort_by_asc: bool = True,
sort_by_field: None | TrunksSortableField = None,
) -> list[dict]:
"""
Implementation of the functionality of API method
`/cloudapi/trunk/list`.
"""
sort_by = None
if sort_by_field:
sort_by_prefix = '+' if sort_by_asc else '-'
sort_by = f'{sort_by_prefix}{sort_by_field.name}'
api_params = {
'account_ids': account_ids,
'ids': ids,
'trunk_tags': vlan_ids,
'status': status.value if status else None,
'page': page_number if page_size else None,
'size': page_size,
'sort_by': sort_by,
}
api_resp = self.decort_api_call(
arg_req_function=requests.get,
arg_api_name='/restmachine/cloudapi/trunk/list',
arg_params=api_params,
)
trunks = api_resp.json()['data']
return trunks
def check_account_vm_features(self, vm_feature: VMFeature) -> bool:
return vm_feature.value in self.acc_info['computeFeatures']
def check_rg_vm_features(self, vm_feature: VMFeature) -> bool:
return vm_feature.value in self.rg_info['computeFeatures']
@waypoint
def extnet_get(self, id: int) -> dict:
"""
Implementation of functionality of the API method
`/cloudapi/extnet/get`.
"""
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/extnet/get',
arg_params={
'net_id': id,
},
not_fail_codes=[404],
)
extnet_info = None
if api_resp.status_code == 200:
extnet_info = api_resp.json()
if not extnet_info:
self.message(
self.MESSAGES.obj_not_found(obj='trunk', id=id)
)
self.exit(fail=True)
return extnet_info
3 weeks ago
@waypoint
def storage_policy_get(self, id: int) -> dict:
"""
Implementation of functionality of the API method
`/cloudapi/storage_policy/get`.
"""
api_resp = self.decort_api_call(
arg_req_function=requests.get,
arg_api_name='/restmachine/cloudapi/storage_policy/get',
arg_params={
'storage_policy_id': id,
},
not_fail_codes=[404],
)
storage_policy_info = None
if api_resp.status_code == 200:
storage_policy_info = api_resp.json()
if not storage_policy_info:
self.message(
self.MESSAGES.obj_not_found(obj='storage_policy', id=id)
)
self.exit(fail=True)
return storage_policy_info
@waypoint
def user_storage_policies(
self,
account_id: None | int = None,
description: None | str = None,
id: None | int = None,
iops_limit: None | int = None,
name: None | str = None,
pool_name: None | str = None,
rg_id: None | int = None,
sep_id: None | int = None,
status: None | StoragePolicyStatus = None,
page_number: int = 1,
page_size: None | int = None,
sort_by_asc: bool = True,
sort_by_field: None | StoragePoliciesSortableField = None,
) -> list[dict]:
"""
Implementation of the functionality of API method
`/cloudapi/storage_policy/list`.
"""
sort_by = None
if sort_by_field:
sort_by_prefix = '+' if sort_by_asc else '-'
sort_by = f'{sort_by_prefix}{sort_by_field.name}'
api_params = {
'account_id': account_id,
'by_id': id,
'name': name,
'status': status.value if status else None,
'desc': description,
'limit_iops': iops_limit,
'resgroup_id': rg_id,
'sep_id': sep_id,
'pool_name': pool_name,
'page': page_number if page_size else None,
'size': page_size,
'sort_by': sort_by,
}
api_resp = self.decort_api_call(
arg_req_function=requests.get,
arg_api_name='/restmachine/cloudapi/storage_policy/list',
arg_params=api_params,
)
storage_policies = api_resp.json()['data']
return storage_policies
@waypoint
def user_security_groups(
self,
account_id: None | int = None,
description: None | str = None,
id: None | int = None,
name: None | str = None,
created_timestamp_min: None | int = None,
created_timestamp_max: None | int = None,
updated_timestamp_min: None | int = None,
updated_timestamp_max: None | int = None,
page_number: int = 1,
page_size: None | int = None,
sort_by_asc: bool = True,
sort_by_field: None | SecurityGroupSortableField = None,
) -> list[dict]:
"""
Implementation of the functionality of API method
`/cloudapi/security_group/list`.
"""
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 = {
'account_id': account_id,
'by_id': id,
'name': name,
'description': description,
'created_min': created_timestamp_min,
'created_max': created_timestamp_max,
'updated_min': updated_timestamp_min,
'updated_max': updated_timestamp_max,
'page': page_number if page_size else None,
'size': page_size,
'sort_by': sort_by,
}
api_resp = self.decort_api_call(
arg_req_function=requests.get,
arg_api_name='/restmachine/cloudapi/security_group/list',
arg_params=api_params,
)
storage_policies = api_resp.json()['data']
return storage_policies
def security_group_find(self, account_id: int, name: str) -> None | dict:
security_groups_by_account_id = self.user_security_groups(
account_id=account_id,
)
for sg in security_groups_by_account_id:
if sg['name'] == name:
return sg
@waypoint
def security_group_get(self, id: int) -> dict:
"""
Implementation of functionality of the API method
`/cloudapi/security_group/get`.
"""
api_resp = self.decort_api_call(
arg_req_function=requests.get,
arg_api_name='/restmachine/cloudapi/security_group/get',
arg_params={
'security_group_id': id,
},
not_fail_codes=[404],
)
storage_policy_info = None
if api_resp.status_code == 200:
storage_policy_info = api_resp.json()
if not storage_policy_info:
self.message(
self.MESSAGES.obj_not_found(obj='security_group', id=id)
)
self.exit(fail=True)
return storage_policy_info
@waypoint
@checkmode
def security_group_create(
self,
account_id: int,
name: str,
description: None | str,
) -> int:
"""
Implementation of functionality of the API method
`/cloudapi/security_group/create`.
"""
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/security_group/create',
arg_params={
'account_id': account_id,
'name': name,
'description': description,
},
)
self.set_changed()
return api_resp.json()
@waypoint
@checkmode
def security_group_detele(self, security_group_id: int) -> bool:
"""
Implementation of functionality of the API method
`/cloudapi/security_group/delete`.
"""
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/security_group/delete',
arg_params={
'security_group_id': security_group_id,
},
)
self.set_changed()
return api_resp.json()
@waypoint
@checkmode
def security_group_update(
self,
security_group_id: int,
name: None | str,
description: None | str,
) -> dict:
"""
Implementation of functionality of the API method
`/cloudapi/security_group/update`.
"""
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/security_group/update',
arg_params={
'security_group_id': security_group_id,
'name': name,
'description': description,
},
)
self.set_changed()
return api_resp.json()
@waypoint
@checkmode
def security_group_create_rule(
self,
security_group_id: int,
direction: SecurityGroupRuleDirection,
ethertype: None | SecurityGroupRuleEtherType,
protocol: None | SecurityGroupRuleProtocol,
port_range_min: None | int,
port_range_max: None | int,
remote_ip_prefix: None | str,
) -> int:
"""
Implementation of functionality of the API method
`/cloudapi/security_group/create_rule`.
"""
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/security_group/create_rule',
arg_params={
'security_group_id': security_group_id,
'direction': direction.value,
'ethertype': ethertype.value if ethertype else None,
'protocol': protocol.value if protocol else None,
'port_range_min': port_range_min,
'port_range_max': port_range_max,
'remote_ip_prefix': remote_ip_prefix,
},
)
self.set_changed()
return api_resp.json()
@waypoint
@checkmode
def security_group_detele_rule(
self,
security_group_id: int,
rule_id: int,
) -> bool:
"""
Implementation of functionality of the API method
`/cloudapi/security_group/delete_rule`.
"""
api_resp = self.decort_api_call(
arg_req_function=requests.post,
arg_api_name='/restmachine/cloudapi/security_group/delete_rule',
arg_params={
'security_group_id': security_group_id,
'rule_id': rule_id,
},
)
self.set_changed()
return api_resp.json()