Initial implementation of ViNS mgmt module and minor bug fixes

master
Sergey Shubin svs1370 5 years ago
parent ce9fb0ceea
commit ca740f98fd

@ -371,7 +371,6 @@ def main():
decon.rg_state(rg_facts, 'enabled')
# TODO: Not sure what to do with the quotas after RG is restored. May need to update rg_facts.
rg_should_exist = True
pass
elif amodule.params['state'] == 'absent':
# destroy permanently
decon.rg_delete(arg_rg_id=rg_id, arg_permanently=True)
@ -390,7 +389,7 @@ def main():
if amodule.params['state'] in ('present', 'enabled'):
# need to re-provision RG
decon.check_amodule_argument('rg_name')
# As we alreafy have validated account ID we can create RG and get rg_id on success
# As we already have validated account ID we can create RG and get rg_id on success
# pass empty string for location code, rg_provision will select the 1st location
rg_id = decon.rg_provision(validated_acc_id,
amodule.params['rg_name'], decon.decort_username,

@ -80,8 +80,8 @@ options:
required: yes
ext_net_id:
description:
- `Controls ViNS connection to an external network. This argument is optional with default value of -1,
which means no external connection.`
- 'Controls ViNS connection to an external network. This argument is optional with default value of -1,
which means no external connection.'
- Specify 0 to connect ViNS to external network and let platform select external network Id automatically.
- Specify positive value to request ViNS connection to the external network with corresponding ID.
- You may also control external IP address selection with I(ext_ip_addr) argument.
@ -91,13 +91,21 @@ options:
description:
- IP address to assign to the external interface of this ViNS when connecting to the external net.
- If empty string is passed, the platform will assign free IP address automatically.
- `Note that if invalid IP address or an address already occupied by another client is specified,
the module will abort with an error.`
- `This argument is used only for new connection to the specified network. You cannot select another
- 'Note that if invalid IP address or an address already occupied by another client is specified,
the module will abort with an error.'
- 'This argument is used only for new connection to the specified network. You cannot select another
external IP address without changing external network ID.'
- ViNS connection to the external network is controlled by I(ext_net_id) argument.
default: empty string
required: no
ipcidr:
description:
- Internal ViNS network address in a format XXX.XXX.XXX.XXX/XX (includes address and netmask).
- If empty string is passed, the platform will assign network address automatically.
- 'When selecting this address manually, note that this address must be unique amomng all ViNSes in
the target account.'
default: empty string
required: no
jwt:
description:
- 'JWT (access token) for authenticating to the DECORT controller when I(authenticator=jwt).'
@ -172,6 +180,10 @@ options:
scenario can lead to security issues, so please know what you are doing.'
default: True
required: no
vins_id:
description:
- ID of the ViNs to manage. If ViNS is identified by ID it must be present.
- If ViNS ID is specified, I(account_id), I(account_name), I(rg_id) and I(rg_name) are ignored.
vins_name:
description:
- Name of the ViNS.
@ -219,6 +231,8 @@ facts:
facts:
id: 5
name: MyViNS
int_net_addr: 192.168.1.0
ext_net_addr: 10.50.11.118
state: CREATED
account_id: 7
rg_id: 19
@ -253,11 +267,27 @@ def decort_vins_package_facts(arg_vins_facts, arg_check_mode=False):
ret_dict['state'] = "ABSENT"
return ret_dict
ret_dict['id'] = arg_rg_facts['id']
ret_dict['name'] = arg_rg_facts['name']
ret_dict['state'] = arg_rg_facts['status']
ret_dict['account_id'] = arg_rg_facts['accountId']
ret_dict['gid'] = arg_rg_facts['gid']
ret_dict['id'] = arg_vins_facts['id']
ret_dict['name'] = arg_vins_facts['name']
ret_dict['state'] = arg_vins_facts['status']
ret_dict['account_id'] = arg_vins_facts['accountId']
ret_dict['rg_id'] = arg_vins_facts['rgid']
ret_dict['int_net_addr'] = arg_vins_facts['network']
ret_dict['gid'] = arg_vins_facts['gid']
if arg_vins_facts['vnfs'].get('GW'):
gw_config = arg_vins_facts['vnfs']['GW']['config']
ret_dict['ext_ip_addr'] = gw_config['ext_net_ip']
ret_dict['ext_net_id'] = gw_config['ext_net_id']
else:
ret_dict['ext_ip_addr'] = ""
ret_dict['ext_net_id'] = -1
# arg_vins_facts['vnfs']['GW']['config']
# ext_ip_addr -> ext_net_ip
# ??? -> ext_net_id
# tech_status -> techStatus
return ret_dict
@ -283,6 +313,7 @@ def decort_vins_parameters():
# datacenter=dict(type='str', required=False, default=''),
ext_net_id=dict(type='int', required=False, default=-1),
ext_ip_addr=dict(type='str', required=False, default=''),
ipcidr=dict(type='str', required=False, default=''),
jwt=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_JWT']),
@ -304,6 +335,7 @@ def decort_vins_parameters():
rg_id=dict(type='int', required=False, default=0),
rg_name=dict(type='str', required=False, default=''),
verify_ssl=dict(type='bool', required=False, default=True),
vins_id=dict(type='int', required=False, default=0),
vins_name=dict(type='str', required=True),
workflow_callback=dict(type='str', required=False),
workflow_context=dict(type='str', required=False),
@ -334,150 +366,228 @@ def main():
decon = DecortController(amodule)
# We need valid Account ID to manage RG.
# Account may be specified either by account_id or account_name. In both cases we
# have to validate account presence and accesibility by the current user.
vins_id = 0
vins_level = "" # "ID" if specified by ID, "RG" - at resource group, "ACC" - at account level
vins_facts = None # will hold ViNS facts
validated_rg_id = 0
rg_facts = None # will hold RG facts
validated_acc_id = 0
if decon.check_amodule_argument('account_id', False):
validated_acc_id, _ = decon.account_find("", amodule.params['account_id'])
else:
decon.check_amodule_argument('account_name') # if no account_name, this function will abort module
validated_acc_id, _ = decon.account_find(amodule.params['account_name'])
acc_facts = None # will hold Account facts
if amodule.params['vins_id']:
# expect existing ViNS with the specified ID
# This call to rg_vins will abort the module if no ViNS with such ID is present
vins_id, vins_facts = decon.vins_find(amodule.params['vins_id'])
if not vins_id:
decon.result['failed'] = True
decon.result['msg'] = "Specified ViNS ID {} not found.".format(amodule.params['vins_id'])
decon.fail_json(**decon.result)
vins_level="ID"
validated_acc_id = vins_facts['accountId']
validated_rg_id = vins_facts['rgid']
elif amodule.params['rg_id']:
# expect ViNS @ RG level in the RG with specified ID
vins_level="RG"
# This call to rg_find will abort the module if no RG with such ID is present
validated_rg_id, rg_facts = decon.rg_find(0, # account ID set to 0 as we search for RG by RG ID
amodule.params['rg_id'], arg_rg_name="")
# This call to vins_find may return vins_id=0 if no ViNS found
vins_id, vins_facts = decon.vins_find(vins_id=0, vins_name=amodule.params['vins_name'],
account_id=0,
rg_id=amodule.params['rg_id'],
check_state=False)
# TODO: add checks and setup ViNS presence flags accordingly
pass
elif amodule.params['account_id'] or amodule.params['account_name'] != "":
# Specified account must be present and accessible by the user, otherwise abort the module
validated_acc_id, acc_facts = decon.account_find(amodule.params['account_name'], amodule.params['account_id'])
if not validated_acc_id:
# we failed to locate account by either name or ID - abort with an error
decon.result['failed'] = True
decon.result['msg'] = ("Current user does not have access to the requested account "
"or non-existent account specified.")
decon.fail_json(**decon.result)
if amodule.params['rg_name'] != "": # at this point we know that rg_id=0
# expect ViNS @ RG level in the RG with specified name under specified account
# RG with the specified name must be present under the account, otherwise abort the module
validated_rg_id, rg_facts = decon.rg_find(validated_acc_id, 0, amodule.params['rg_name'])
if (not validated_rg_id or
rg_facts['status'] in ["DESTROYING", "DESTROYED", "DELETING", "DELETED", "DISABLING", "ENABLING"]):
decon.result['failed'] = True
decon.result['msg'] = "RG name '{}' not found or has invalid state.".format(amodule.params['rg_name'])
decon.fail_json(**decon.result)
# This call to vins_find may return vins_id=0 if no ViNS with this name found under specified RG
vins_id, vins_facts = decon.vins_find(vins_id=0, vins_name=amodule.params['vins_name'],
account_id=0, # set to 0, as we are looking for ViNS under RG
rg_id=validated_rg_id,
check_state=False)
vins_level = "RG"
# TODO: add checks and setup ViNS presence flags accordingly
else: # At this point we know for sure that rg_name="" and rg_id=0
# So we expect ViNS @ account level
# This call to vins_find may return vins_id=0 if no ViNS found
vins_id, vins_facts = decon.vins_find(vins_id=0, vins_name=amodule.params['vins_name'],
account_id=validated_acc_id,
rg_id=0,
check_state=False)
vins_level = "ACC"
# TODO: add checks and setup ViNS presence flags accordingly
else:
# this is "invalid arguments combination" sink
# if we end up here, it means that module was invoked with vins_id=0 and rg_id=0
decon.result['failed'] = True
if amodule.params['account_id'] == 0 and amodule.params['account_name'] == "":
decon.result['msg'] = "Cannot find ViNS by name when account name is empty and account ID is 0."
if amodule.params['rg_name'] != "":
# rg_name without account specified
decon.result['msg'] = "Cannot find ViNS by name when RG name is empty and RG ID is 0."
decon.fail_json(**decon.result)
# Check if the RG with the specified parameters already exists
rg_id, rg_facts = decon.rg_find(validated_acc_id,
0, arg_rg_name=amodule.params['rg_name'],
arg_check_state=False)
rg_should_exist = True
#
# Initial validation of module arguments is complete
#
# At this point non-zero vins_id means that we will be managing pre-existing ViNS
# Otherwise we are about to create a new one as follows:
# - if validated_rg_id is non-zero, create ViNS @ RG level
# - if validated_rg_id is zero, create ViNS @ account level
#
# When managing existing ViNS we need to account for both "static" and "transient"
# status. Full range of ViNS statii is as follows:
#
# "MODELED", "CREATED", "ENABLED", "ENABLING", "DISABLED", "DISABLING", "DELETED", "DELETING", "DESTROYED", "DESTROYING"
#
vins_should_exist = False
if rg_id:
if rg_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING"]:
# error: nothing can be done to existing RG in the listed statii regardless of
if vins_id:
vins_should_exist = True
if vins_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING"]:
# error: nothing can be done to existing ViNS in the listed statii regardless of
# the requested state
decon.result['failed'] = True
decon.result['changed'] = False
decon.result['msg'] = ("No change can be done for existing RG ID {} because of its current "
"status '{}'").format(rg_id, rg_facts['status'])
elif rg_facts['status'] == "DISABLED":
decon.result['msg'] = ("No change can be done for existing ViNS ID {} because of its current "
"status '{}'").format(vins_id, vins_facts['status'])
elif vins_facts['status'] == "DISABLED":
if amodule.params['state'] == 'absent':
decon.rg_delete(arg_rg_id=rg_id, arg_permanently=True)
rg_facts['status'] = 'DESTROYED'
rg_should_exist = False
decon.vins_delete(vins_id, permanently=True)
vins_facts['status'] = 'DESTROYED'
vins_should_exist = False
elif amodule.params['state'] in ('present', 'disabled'):
# update quotas
decon.rg_quotas(rg_facts, amodule.params['quotas'])
# update ViNS, leave in disabled state
decon.vins_update(vins_facts,
amodule.params['ext_net_id'], amodule.params['ext_ip_addr'])
elif amodule.params['state'] == 'enabled':
# update quotas and enable
decon.rg_quotas(rg_facts, amodule.params['quotas'])
decon.rg_state(rg_facts, 'enabled')
elif rg_facts['status'] == "CREATED":
# update ViNS and enable
decon.vins_update(vins_facts,
amodule.params['ext_net_id'], amodule.params['ext_ip_addr'])
decon.vins_state(vins_facts, 'enabled')
elif vins_facts['status'] in ["CREATED", "ENABLED"]:
if amodule.params['state'] == 'absent':
decon.rg_delete(arg_rg_id=rg_id, arg_permanently=True)
rg_facts['status'] = 'DESTROYED'
rg_should_exist = False
decon.vins_delete(vins_id, permanently=True)
vins_facts['status'] = 'DESTROYED'
vins_should_exist = False
elif amodule.params['state'] in ('present', 'enabled'):
# update quotas
decon.rg_quotas(rg_facts, amodule.params['quotas'])
# update ViNS
decon.vins_update(vins_facts,
amodule.params['ext_net_id'], amodule.params['ext_ip_addr'])
elif amodule.params['state'] == 'disabled':
# disable and update quotas
decon.rg_state(rg_facts, 'disabled')
decon.rg_quotas(rg_facts, amodule.params['quotas'])
elif rg_facts['status'] == "DELETED":
# disable and update ViNS
decon.vins_state(vins_facts, 'disabled')
decon.vins_update(vins_facts,
amodule.params['ext_net_id'], amodule.params['ext_ip_addr'])
elif vins_facts['status'] == "DELETED":
if amodule.params['state'] in ['present', 'enabled']:
# restore and enable
# TODO: check if restore RG API returns the new RG ID of the restored RG instance.
decon.rg_restore(arg_rg_id=rg_id)
decon.rg_state(rg_facts, 'enabled')
# TODO: Not sure what to do with the quotas after RG is restored. May need to update rg_facts.
rg_should_exist = True
pass
decon.vins_restore(arg_vins_id=vins_id)
decon.vins_state(vins_facts, 'enabled')
vins_should_exist = True
elif amodule.params['state'] == 'absent':
# destroy permanently
decon.rg_delete(arg_rg_id=rg_id, arg_permanently=True)
rg_facts['status'] = 'DESTROYED'
rg_should_exist = False
decon.vins_delete(vins_id, permanently=True)
vins_facts['status'] = 'DESTROYED'
vins_should_exist = False
elif amodule.params['state'] == 'disabled':
# error
decon.result['failed'] = True
decon.result['changed'] = False
decon.result['msg'] = ("Invalid target state '{}' requested for RG ID {} in the "
"current status '{}'").format(rg_id,
decon.result['msg'] = ("Invalid target state '{}' requested for ViNS ID {} in the "
"current status '{}'").format(vins_id,
amodule.params['state'],
rg_facts['status'])
rg_should_exist = False
elif rg_facts['status'] == "DESTROYED":
vins_facts['status'])
vins_should_exist = False
elif vins_facts['status'] == "DESTROYED":
if amodule.params['state'] in ('present', 'enabled'):
# need to re-provision RG
decon.check_amodule_argument('rg_name')
# As we alreafy have validated account ID we can create RG and get rg_id on success
# pass empty string for location code, rg_provision will select the 1st location
rg_id = decon.rg_provision(validated_acc_id,
amodule.params['rg_name'], decon.decort_username,
amodule.params['quotas'])
rg_should_exist = True
# need to re-provision ViNS; some attributes may be changed, some stay the same.
# account and RG - stays the same
# vins_name - stays the same
# IPcidr - take from module arguments
# ext IP address - take from module arguments
# annotation - take from module arguments
vins_id = decon.vins_provision(vins_facts['name'],
validated_acc_id, validated_rg_id,
amodule.params['ipcidr'],
amodule.params['ext_net_id'], amodule.params['ext_ip_addr'],
amodule.params['annotation'])
vins_should_exist = True
elif amodule.params['state'] == 'absent':
# nop
decon.result['failed'] = False
decon.result['changed'] = False
decon.result['msg'] = ("No state change required for RG ID {} because of its "
"current status '{}'").format(rg_id,
rg_facts['status'])
rg_should_exist = False
decon.result['msg'] = ("No state change required for ViNS ID {} because of its "
"current status '{}'").format(vins_id,
vins_facts['status'])
vins_should_exist = False
elif amodule.params['state'] == 'disabled':
# error
decon.result['failed'] = True
decon.result['changed'] = False
decon.result['msg'] = ("Invalid target state '{}' requested for RG ID {} in the "
"current status '{}'").format(rg_id,
decon.result['msg'] = ("Invalid target state '{}' requested for ViNS ID {} in the "
"current status '{}'").format(vins_id,
amodule.params['state'],
rg_facts['status'])
vins_facts['status'])
else:
# Preexisting RG was not found.
rg_should_exist = False # we will change it back to True if RG is explicitly created or restored
# Preexisting ViNS was not found.
vins_should_exist = False # we will change it back to True if ViNS is created or restored
# If requested state is 'absent' - nothing to do
if amodule.params['state'] == 'absent':
decon.result['failed'] = False
decon.result['changed'] = False
decon.result['msg'] = ("Nothing to do as target state 'absent' was requested for "
"non-existent RG name '{}'").format(amodule.params['rg_name'])
"non-existent ViNS name '{}'").format(amodule.params['vins_name'])
elif amodule.params['state'] in ('present', 'enabled'):
# Target RG does not exist yet - create it and store the returned ID in rg_id variable for later use
# To create RG we need account name (or account ID) and RG name - check
# that these parameters are present and proceed.
decon.check_amodule_argument('rg_name')
# as we already have account ID we can create RG and get rg_id on success
# pass empty string for location code, rg_provision will select the 1st location
rg_id = decon.rg_provision(validated_acc_id,
amodule.params['rg_name'], decon.decort_username,
amodule.params['quotas'])
rg_should_exist = True
decon.check_amodule_argument('vins_name')
# as we already have account ID and RG ID we can create ViNS and get vins_id on success
vins_id = decon.vins_provision(amodule.params['vins_name'],
validated_acc_id, validated_rg_id,
amodule.params['ipcidr'],
amodule.params['ext_net_id'], amodule.params['ext_ip_addr'],
amodule.params['annotation'])
vins_should_exist = True
elif amodule.params['state'] == 'disabled':
decon.result['failed'] = True
decon.result['changed'] = False
decon.result['msg'] = ("Invalid target state '{}' requested for non-existent "
"RG name '{}' ").format(amodule.params['state'],
amodule.params['rg_name'])
"ViNS name '{}'").format(amodule.params['state'],
amodule.params['vins_name'])
#
# conditional switch end - complete module run
#
if decon.result['failed']:
amodule.fail_json(**decon.result)
else:
# prepare RG facts to be returned as part of decon.result and then call exit_json(...)
# rg_facts = None
if rg_should_exist:
# prepare ViNS facts to be returned as part of decon.result and then call exit_json(...)
if vins_should_exist:
if decon.result['changed']:
# If we arrive here, there is a good chance that the RG is present - get fresh RG facts from
# the cloud by RG ID.
# Otherwise, RG facts from previous call (when the RG was still in existence) will be returned.
_, rg_facts = decon.rg_find(arg_account_id=0, arg_rg_id=rg_id)
decon.result['facts'] = decort_rg_package_facts(rg_facts, amodule.check_mode)
# If we arrive here, there is a good chance that the ViNS is present - get fresh ViNS
# facts from # the cloud by ViNS ID.
# Otherwise, ViNS facts from previous call (when the ViNS was still in existence) will
# be returned.
_, vins_facts = decon.vins_find(vins_id)
decon.result['facts'] = decort_vins_package_facts(vins_facts, amodule.check_mode)
amodule.exit_json(**decon.result)

@ -1257,6 +1257,11 @@ class DecortController(object):
ret_rg_id = 0
ret_rg_dict = dict()
if not arg_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 = dict(rgId=arg_rg_id,)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/get", api_params)
if api_resp.status_code == 200:
@ -1275,12 +1280,13 @@ class DecortController(object):
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.
@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: boolean that tells the method to report RGs in valid states only.
@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.
@ -1289,7 +1295,7 @@ class DecortController(object):
# 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 manipularion viewpoint
# Transient state (ending with ING) are invalid from RG manipulation viewpoint
#
RG_INVALID_STATES = ["MODELED"]
@ -1317,7 +1323,7 @@ class DecortController(object):
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 gettin all RGs IDs within the specified account
# try to locate RG by name - start with getting all RGs IDs within the specified account
api_params['accountId'] = arg_account_id
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/accounts/get", api_params)
if api_resp.status_code == 200:
@ -1416,7 +1422,7 @@ class DecortController(object):
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/cloudspaces/create returns ID of the newly created VDC on success
# 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'))
@ -1510,8 +1516,8 @@ class DecortController(object):
def rg_state(self, arg_rg_dict, arg_desired_state):
"""Enable or disable RG.
@param arg_rg_dict: dictionary with the target VDC facts as returned by vdc_find(...) method or
.../rg/get API call to obtain the data.
@param arg_rg_dict: dictionary with the target RG facts as returned by rg_find(...) method or
.../rg/get API call.
@param arg_desired_state: the desired state for this RG. Valid states are 'enabled' and 'disabled'.
"""
@ -1617,20 +1623,22 @@ class DecortController(object):
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/accounts/get", api_params)
if api_resp.status_code == 200:
account_details = json.loads(api_resp.content.decode('utf8'))
account_record = {
'id': account_details['id'],
'name': account_details['name'],
}
return account_details['id'], account_record
return account_details['id'], account_details
else:
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/accounts/list", api_params)
if api_resp.status_code == 200:
# Parse response to see if a account matching arg_account_name is found in the output
# If it is found, assign its ID to the return variable and copy dictionary with the facts
accounts_list = json.loads(api_resp.content.decode('utf8'))
for account_record in accounts_list:
if account_record['name'] == arg_account_name:
return account_record['id'], account_record
for runner in accounts_list:
if runner['name'] == arg_account_name:
# get detailed information about the account from "accounts/get" call as
# "accounts/list" does not return all necessary fields
api_params['accountId'] = runner['id']
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/accounts/get", api_params)
if api_resp.status_code == 200:
account_details = json.loads(api_resp.content.decode('utf8'))
return account_details['id'], account_details
return 0, None
@ -1790,3 +1798,390 @@ class DecortController(object):
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['changed'] = False
self.result['msg'] = "vins_delete() in check mode: delete ViNS ID {} was requested.".format(vins_id)
return
#
# TODO: need decision if deleting a VINS with connected computes is allowed (aka force=True)
# and implement this decision accordingly.
#
api_params = dict(vinsId=vins_id,
# force=True | False,
permanently=permanently,)
self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/delete", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False
self.result['changed'] = True
return
def _vins_get_by_id(self, vins_id):
"""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)
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 vins_find(self, vins_id, vins_name="", account_id=0, rg_id=0, 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
VINS_INVALID_STATES = ["ENABLING", "DISABLING", "DELETING", "DELETED", "DESTROYING", "DESTROYED"]
ret_vins_id = 0
# api_params = dict()
ret_vins_facts = None
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_find")
if vins_id > 0:
ret_vins_id, ret_vins_facts = self._vins_get_by_id(vins_id)
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 account_id > 0:
# ignore rg_id and 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)
# TODO: account's 'vins' attribute does not list deleted or destroyed ViNSes!
for runner in validated_facts['vins']:
# api_params['vinsId'] = runner
ret_vins_id, ret_vins_facts = self._vins_get_by_id(runner)
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
elif 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)
# TODO: RG's 'vins' attribute does not list deleted or destroyed ViNSes!
for runner in validated_facts['vins']:
# api_params['vinsId'] = runner
ret_vins_id, ret_vins_facts = self._vins_get_by_id(runner)
if ret_vins_id and ret_vins_facts['name'] == vins_name:
if not check_state or ret_vins_facts['status'] not in VINS_INVALID_STATES:
return ret_vins_id, ret_vins_facts
else:
return 0, None
else: # both Account ID and RG ID are zero - fail the module
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)
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 with zero ID and empty name."
self.amodule.fail_json(**self.result)
return 0, None
def vins_provision(self, vins_name, account_id, rg_id=0, ipcidr="", ext_net_id=-1, ext_ip_addr="", desc=""):
"""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['changed'] = 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
api_params['desc'] = desc
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['changed'] = False
self.result['msg'] = "vins_restore() in check mode: restore ViNS ID {} was requested.".format(vins_id)
return
api_params = dict(vinsId=vins_id,
reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username),)
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")
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,
vins_dict['id'])
return
if self.amodule.check_mode:
self.result['failed'] = False
self.result['changed'] = False
self.result['msg'] = ("vins_state() in check mode: setting state of ViNS ID {}, name '{}' to "
"'{}' was requested.").format(vins_dict['id'],vins_dict['name'],
desired_state)
return
vinsstate_api = "" # this string will also be used as a flag to indicate that API call is necessary
api_params = dict(vinsId=vins_dict['id'],
reason='Changed by DECORT Ansible module, vins_state method.')
if vins_dict['status'] in ["CREATED", "ENABLED"] and desired_state == 'disabled':
rgstate_api = "/restmachine/cloudapi/vins/disable"
elif vins_dict['status'] == "DISABLED" and desired_state == 'enabled':
rgstate_api = "/restmachine/cloudapi/vins/enable"
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
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(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.
"""
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['changed'] = False
self.result['msg'] = ("vins_update() in check mode: updating ViNS ID {}, name '{}' "
"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['changed'] = False
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)
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['changed'] = False
self.result['failed'] = False
self.result['warning'] = ("vins_update(): ViNS ID {} is already connected to ext net ID {}, "
"no reconnection to default network will be done.").format(vins_dict['id'],
gw_config['ext_net_id'])
return

Loading…
Cancel
Save