Update README, add kubernetes support

rc-4.0
Aleksandr Malyavin 3 years ago
parent 36773c7bb3
commit 587f0d9c0b

@ -5,8 +5,8 @@ Note that this module may produce unreliable results when used with older DECORT
Requirements: Requirements:
* Ansible 2.7 or higher * Ansible 2.7 or higher
* Python 2.6 or higher * Python 3.7 or higher
* PyJWT 1.7.1 Python module * PyJWT 2.0.0 Python module or higher
* requests Python module * requests Python module
* netaddr Python module * netaddr Python module
* DECORT cloud platform version 3.5.0 or higher * DECORT cloud platform version 3.5.0 or higher

@ -188,3 +188,136 @@
var: my_pfw.facts var: my_pfw.facts
delegate_to: localhost delegate_to: localhost
- name: Create k8s cluster with params
decort_k8s:
authenticator: jwt
jwt: "{{ token.jwt }}"
controller_url: "{{ decort_ctrl }}"
k8s_name: "k8s_cluster_name"
wg_name: "k8s_wg_name"
k8ci_id: "{{ k8ci_id }}"
rg_id: "{{ my_rg.facts.id }}"
master_count: 1
master_cpu: 2
master_ram_mb: 2048
master_disk_gb: 20
worker_count: 3
worker_cpu: 1
worker_ram_mb: 1024
worker_disk_gb: 20
extnet_id: "{{ target_ext_net_id }}"
with_lb: True
state: present
register: k8s
delegate_to: localhost
- name: print out the result
debug:
var: k8s
delegate_to: localhost
- name: Disable k8s cluster
decort_k8s:
authenticator: jwt
jwt: "{{ token.jwt }}"
controller_url: "{{ decort_ctrl }}"
k8s_name: "k8s_cluster_name"
wg_name: "k8s_wg_name"
k8ci_id: "{{ k8ci_id }}"
rg_id: "{{ my_rg.facts.id }}"
state: disabled
register: k8s
delegate_to: localhost
- name: print out the result
debug:
var: k8s
delegate_to: localhost
- name: Delete in trash k8s cluster
decort_k8s:
authenticator: jwt
jwt: "{{ token.jwt }}"
controller_url: "{{ decort_ctrl }}"
k8s_name: "k8s_cluster_name"
wg_name: "k8s_wg_name"
k8ci_id: "{{ k8ci_id }}"
rg_id: "{{ my_rg.facts.id }}"
state: absent
permanent: False
register: k8s
delegate_to: localhost
- name: print out the result
debug:
var: k8s
delegate_to: localhost
- name: Restore from trash deleted k8s cluster
decort_k8s:
authenticator: jwt
jwt: "{{ token.jwt }}"
controller_url: "{{ decort_ctrl }}"
k8s_name: "k8s_cluster_name"
wg_name: "k8s_wg_name"
k8ci_id: "{{ k8ci_id }}"
rg_id: "{{ my_rg.facts.id }}"
state: enabled
register: k8s
delegate_to: localhost
- name: print out the result
debug:
var: k8s
delegate_to: localhost
- name: Enable k8s cluster
decort_k8s:
authenticator: jwt
jwt: "{{ token.jwt }}"
controller_url: "{{ decort_ctrl }}"
k8s_name: "k8s_cluster_name"
wg_name: "k8s_wg_name"
k8ci_id: "{{ k8ci_id }}"
rg_id: "{{ my_rg.facts.id }}"
state: enabled
register: k8s
delegate_to: localhost
- name: Enable k8s cluster
decort_k8s:
authenticator: jwt
jwt: "{{ token.jwt }}"
controller_url: "{{ decort_ctrl }}"
k8s_name: "k8s_cluster_name"
wg_name: "k8s_wg_name"
k8ci_id: "{{ k8ci_id }}"
rg_id: "{{ my_rg.facts.id }}"
state: enabled
started: True
register: k8s
delegate_to: localhost
- name: print out the result
debug:
var: k8s
delegate_to: localhost
- name: Destroy k8s cluster
decort_k8s:
authenticator: jwt
jwt: "{{ token.jwt }}"
controller_url: "{{ decort_ctrl }}"
k8s_name: "k8s_cluster_name"
wg_name: "k8s_wg_name"
k8ci_id: "{{ k8ci_id }}"
rg_id: "{{ my_rg.facts.id }}"
state: absent
permanent: True
register: k8s
delegate_to: localhost
- name: print out the result
debug:
var: k8s
delegate_to: localhost

@ -0,0 +1,236 @@
#!/usr/bin/python
#
# Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible
# Copyright: (c) 2018-2021 Digital Energy Cloud Solutions LLC
#
# Apache License 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.txt)
#
#
# Author: Aleksandr Malyavin (aleksandr.malyavin@digitalenergy.online)
#
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import env_fallback
from ansible.module_utils.decort_utils import *
def decort_k8s_package_facts(arg_k8s_facts, arg_check_mode=False):
"""Package a dictionary of k8s facts according to the decort_k8s module specification. This dictionary will
be returned to the upstream Ansible engine at the completion of the module run.
@param arg_k8s_facts: dictionary with k8s facts as returned by API call to .../k8s/get
@param arg_check_mode: boolean that tells if this Ansible module is run in check mode
"""
ret_dict = dict(id=0,
name="none",
state="CHECK_MODE",
)
if arg_check_mode:
# in check mode return immediately with the default values
return ret_dict
if arg_k8s_facts is None:
# if void facts provided - change state value to ABSENT and return
ret_dict['state'] = "ABSENT"
return ret_dict
ret_dict['id'] = arg_k8s_facts['id']
ret_dict['name'] = arg_k8s_facts['name']
ret_dict['techStatus'] = arg_k8s_facts['techStatus']
ret_dict['state'] = arg_k8s_facts['status']
return ret_dict
def decort_k8s_parameters():
"""Build and return a dictionary of parameters expected by decort_k8s module in a form accepted
by AnsibleModule utility class."""
return dict(
account_id=dict(type='int', required=False),
account_name=dict(type='str', required=False, default=''),
annotation=dict(type='str', required=False, default=''),
app_id=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_APP_ID'])),
app_secret=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_APP_SECRET']),
no_log=True),
authenticator=dict(type='str',
required=True,
choices=['legacy', 'oauth2', 'jwt']),
controller_url=dict(type='str', required=True),
# datacenter=dict(type='str', required=False, default=''),
jwt=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_JWT']),
no_log=True),
oauth2_url=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_OAUTH2_URL'])),
password=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_PASSWORD']),
no_log=True),
quotas=dict(type='dict', required=False),
state=dict(type='str',
default='present',
choices=['absent', 'disabled', 'enabled', 'present']),
permanent=dict(type='bool', default=False),
started=dict(type='bool', default=True),
user=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_USER'])),
k8s_name=dict(type='str', required=True),
rg_id=dict(type='int', required=True),
k8ci_id=dict(type='int', required=True),
wg_name=dict(type='str', required=True),
master_count=dict(type='int', default=1),
master_cpu=dict(type='int', default=2),
master_ram_mb=dict(type='int', default=2048),
master_disk_gb=dict(type='int', default=10),
worker_count=dict(type='int', default=1),
worker_cpu=dict(type='int', default=1),
worker_ram_mb=dict(type='int', default=1024),
worker_disk_gb=dict(type='int', default=0),
extnet_id=dict(type='int', default=0),
description=dict(type='str', default="Created by decort ansible module"),
with_lb=dict(type='bool', default=True),
verify_ssl=dict(type='bool', required=False, default=True),
workflow_callback=dict(type='str', required=False),
workflow_context=dict(type='str', required=False),
)
def main():
module_parameters = decort_k8s_parameters()
amodule = AnsibleModule(argument_spec=module_parameters,
supports_check_mode=True,
mutually_exclusive=[
['oauth2', 'password'],
['password', 'jwt'],
['jwt', 'oauth2'],
],
required_together=[
['app_id', 'app_secret'],
['user', 'password'],
],
)
decon = DecortController(amodule)
k8s_id, k8s_facts = decon.k8s_find(arg_k8s_name=amodule.params['k8s_name'],
arg_check_state=False)
k8s_should_exist = True
if k8s_id:
if k8s_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING", "CREATING",
"RESTORING"] and amodule.params['state'] != "present":
decon.result['failed'] = True
decon.result['changed'] = False
decon.result['msg'] = ("No change can be done for existing k8s ID {} because of its current "
"status '{}'").format(k8s_id, k8s_facts['status'])
elif k8s_facts['status'] in ["DISABLED", "ENABLED", "CREATED", "DELETED"] and amodule.params['state'] == "absent":
if amodule.params['permanent'] is True:
decon.k8s_delete(k8s_id, True)
k8s_facts['status'] = 'DESTROYED'
k8s_should_exist = False
else:
decon.k8s_delete(k8s_id)
k8s_facts['status'] = 'DELETED'
k8s_should_exist = True
elif k8s_facts['status'] == "ENABLED" and amodule.params['started'] is True:
decon.k8s_state(k8s_facts, amodule.params['state'], amodule.params['started'])
elif k8s_facts['status'] == amodule.params['state'].upper():
decon.k8s_state(k8s_facts, amodule.params['state'])
elif k8s_facts['status'] in ["ENABLED", "CREATED"] and amodule.params['state'] == "disabled":
decon.k8s_state(k8s_facts, 'disabled')
elif k8s_facts['status'] in ["DISABLED", "CREATED"]:
if amodule.params['state'] == 'enabled':
decon.k8s_state(k8s_facts, 'enabled', amodule.params['started'])
elif amodule.params['state'] == "disabled":
decon.k8s_state(k8s_facts, 'disabled')
k8s_should_exist = True
elif k8s_facts['status'] == "DELETED":
if amodule.params['state'] in ('enabled', 'present'):
decon.k8s_restore(k8s_id)
k8s_should_exist = True
elif amodule.params['state'] == 'disabled':
decon.result['failed'] = True
decon.result['changed'] = False
decon.result['msg'] = ("Invalid target state '{}' requested for k8s ID {} in the "
"current status '{}'").format(k8s_id,
amodule.params['state'],
k8s_facts['status'])
k8s_should_exist = False
elif k8s_facts['status'] == "DESTROYED":
if amodule.params['state'] in ('present', 'enabled'):
k8s_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 k8s ID {} because of its "
"current status '{}'").format(k8s_id,
k8s_facts['status'])
k8s_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 k8s ID {} in the "
"current status '{}'").format(k8s_id,
amodule.params['state'],
k8s_facts['status'])
else:
k8s_should_exist = False
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 k8s name '{}'").format(amodule.params['k8s_name'])
elif amodule.params['state'] in ('present', 'enabled'):
decon.check_amodule_argument('k8s_name')
k8s_id = decon.k8s_provision(amodule.params['k8s_name'],
amodule.params['wg_name'],
amodule.params['k8ci_id'],
amodule.params['rg_id'],
amodule.params['master_count'],
amodule.params['master_cpu'],
amodule.params['master_ram_mb'],
amodule.params['master_disk_gb'],
amodule.params['worker_count'],
amodule.params['worker_cpu'],
amodule.params['worker_ram_mb'],
amodule.params['worker_disk_gb'],
amodule.params['extnet_id'],
amodule.params['with_lb'],
amodule.params['description'],
)
k8s_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 "
"k8s name '{}' ").format(amodule.params['state'],
amodule.params['k8s_name'])
if decon.result['failed']:
amodule.fail_json(**decon.result)
else:
if k8s_should_exist:
if decon.result['changed']:
_, k8s_facts = decon.k8s_find(arg_k8s_id=k8s_id)
decon.result['facts'] = decort_k8s_package_facts(k8s_facts, amodule.check_mode)
amodule.exit_json(**decon.result)
if __name__ == "__main__":
main()

@ -284,15 +284,15 @@ def decort_vins_package_facts(arg_vins_facts, arg_check_mode=False):
else: else:
ret_dict['ext_ip_addr'] = "" ret_dict['ext_ip_addr'] = ""
ret_dict['ext_net_id'] = -1 ret_dict['ext_net_id'] = -1
# arg_vins_facts['vnfs']['GW']['config'] # arg_vins_facts['vnfs']['GW']['config']
# ext_ip_addr -> ext_net_ip # ext_ip_addr -> ext_net_ip
# ??? -> ext_net_id # ??? -> ext_net_id
# tech_status -> techStatus # tech_status -> techStatus
return ret_dict return ret_dict
def decort_vins_parameters(): def decort_vins_parameters():
"""Build and return a dictionary of parameters expected by decort_vins module in a form accepted """Build and return a dictionary of parameters expected by decort_vins module in a form accepted
by AnsibleModule utility class.""" by AnsibleModule utility class."""
@ -342,6 +342,7 @@ def decort_vins_parameters():
workflow_context=dict(type='str', required=False), workflow_context=dict(type='str', required=False),
) )
# Workflow digest: # Workflow digest:
# 1) authenticate to DECORT controller & validate authentication by issuing API call - done when creating DECORTController # 1) authenticate to DECORT controller & validate authentication by issuing API call - done when creating DECORTController
# 2) check if the ViNS with this id or name exists under specified account / resource group # 2) check if the ViNS with this id or name exists under specified account / resource group
@ -368,12 +369,12 @@ def main():
decon = DecortController(amodule) decon = DecortController(amodule)
vins_id = 0 vins_id = 0
vins_level = "" # "ID" if specified by ID, "RG" - at resource group, "ACC" - at account level vins_level = "" # "ID" if specified by ID, "RG" - at resource group, "ACC" - at account level
vins_facts = None # will hold ViNS facts vins_facts = None # will hold ViNS facts
validated_rg_id = 0 validated_rg_id = 0
rg_facts = None # will hold RG facts rg_facts = None # will hold RG facts
validated_acc_id = 0 validated_acc_id = 0
acc_facts = None # will hold Account facts acc_facts = None # will hold Account facts
if amodule.params['vins_id']: if amodule.params['vins_id']:
# expect existing ViNS with the specified ID # expect existing ViNS with the specified ID
@ -383,56 +384,56 @@ def main():
decon.result['failed'] = True decon.result['failed'] = True
decon.result['msg'] = "Specified ViNS ID {} not found.".format(amodule.params['vins_id']) decon.result['msg'] = "Specified ViNS ID {} not found.".format(amodule.params['vins_id'])
decon.fail_json(**decon.result) decon.fail_json(**decon.result)
vins_level="ID" vins_level = "ID"
validated_acc_id = vins_facts['accountId'] validated_acc_id = vins_facts['accountId']
validated_rg_id = vins_facts['rgId'] validated_rg_id = vins_facts['rgId']
elif amodule.params['rg_id']: elif amodule.params['rg_id']:
# expect ViNS @ RG level in the RG with specified ID # expect ViNS @ RG level in the RG with specified ID
vins_level="RG" vins_level = "RG"
# This call to rg_find will abort the module if no RG with such ID is present # 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 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="") amodule.params['rg_id'], arg_rg_name="")
# This call to vins_find may return vins_id=0 if no ViNS found # 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'], vins_id, vins_facts = decon.vins_find(vins_id=0, vins_name=amodule.params['vins_name'],
account_id=0, account_id=0,
rg_id=amodule.params['rg_id'], rg_id=amodule.params['rg_id'],
check_state=False) check_state=False)
# TODO: add checks and setup ViNS presence flags accordingly # TODO: add checks and setup ViNS presence flags accordingly
pass pass
elif amodule.params['account_id'] or amodule.params['account_name'] != "": elif amodule.params['account_id'] or amodule.params['account_name'] != "":
# Specified account must be present and accessible by the user, otherwise abort the module # 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']) validated_acc_id, acc_facts = decon.account_find(amodule.params['account_name'], amodule.params['account_id'])
if not validated_acc_id: if not validated_acc_id:
decon.result['failed'] = True decon.result['failed'] = True
decon.result['msg'] = ("Current user does not have access to the requested account " decon.result['msg'] = ("Current user does not have access to the requested account "
"or non-existent account specified.") "or non-existent account specified.")
decon.fail_json(**decon.result) decon.fail_json(**decon.result)
if amodule.params['rg_name'] != "": # at this point we know that rg_id=0 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 # 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 # 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']) validated_rg_id, rg_facts = decon.rg_find(validated_acc_id, 0, amodule.params['rg_name'])
if (not validated_rg_id or if (not validated_rg_id or
rg_facts['status'] in ["DESTROYING", "DESTROYED", "DELETING", "DELETED", "DISABLING", "ENABLING"]): rg_facts['status'] in ["DESTROYING", "DESTROYED", "DELETING", "DELETED", "DISABLING", "ENABLING"]):
decon.result['failed'] = True decon.result['failed'] = True
decon.result['msg'] = "RG name '{}' not found or has invalid state.".format(amodule.params['rg_name']) decon.result['msg'] = "RG name '{}' not found or has invalid state.".format(amodule.params['rg_name'])
decon.fail_json(**decon.result) 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 # 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'], 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 account_id=0, # set to 0, as we are looking for ViNS under RG
rg_id=validated_rg_id, rg_id=validated_rg_id,
check_state=False) check_state=False)
vins_level = "RG" vins_level = "RG"
# TODO: add checks and setup ViNS presence flags accordingly # TODO: add checks and setup ViNS presence flags accordingly
else: # At this point we know for sure that rg_name="" and rg_id=0 else: # At this point we know for sure that rg_name="" and rg_id=0
# So we expect ViNS @ account level # So we expect ViNS @ account level
# This call to vins_find may return vins_id=0 if no ViNS found # 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'], vins_id, vins_facts = decon.vins_find(vins_id=0, vins_name=amodule.params['vins_name'],
account_id=validated_acc_id, account_id=validated_acc_id,
rg_id=0, rg_id=0,
check_state=False) check_state=False)
vins_level = "ACC" vins_level = "ACC"
# TODO: add checks and setup ViNS presence flags accordingly # TODO: add checks and setup ViNS presence flags accordingly
else: else:
# this is "invalid arguments combination" sink # 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 # if we end up here, it means that module was invoked with vins_id=0 and rg_id=0
decon.result['failed'] = True decon.result['failed'] = True
@ -453,12 +454,12 @@ def main():
# #
# When managing existing ViNS we need to account for both "static" and "transient" # When managing existing ViNS we need to account for both "static" and "transient"
# status. Full range of ViNS statii is as follows: # status. Full range of ViNS statii is as follows:
# #
# "MODELED", "CREATED", "ENABLED", "ENABLING", "DISABLED", "DISABLING", "DELETED", "DELETING", "DESTROYED", "DESTROYING" # "MODELED", "CREATED", "ENABLED", "ENABLING", "DISABLED", "DISABLING", "DELETED", "DELETING", "DESTROYED", "DESTROYING"
# #
vins_should_exist = False vins_should_exist = False
if vins_id: if vins_id:
vins_should_exist = True vins_should_exist = True
if vins_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING"]: if vins_facts['status'] in ["MODELED", "DISABLING", "ENABLING", "DELETING", "DESTROYING"]:
@ -526,7 +527,7 @@ def main():
# annotation - take from module arguments # annotation - take from module arguments
vins_id = decon.vins_provision(vins_facts['name'], vins_id = decon.vins_provision(vins_facts['name'],
validated_acc_id, validated_rg_id, validated_acc_id, validated_rg_id,
amodule.params['ipcidr'], amodule.params['ipcidr'],
amodule.params['ext_net_id'], amodule.params['ext_ip_addr'], amodule.params['ext_net_id'], amodule.params['ext_ip_addr'],
amodule.params['annotation']) amodule.params['annotation'])
vins_should_exist = True vins_should_exist = True
@ -559,18 +560,17 @@ def main():
decon.check_amodule_argument('vins_name') 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 # 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'], vins_id = decon.vins_provision(amodule.params['vins_name'],
validated_acc_id, validated_rg_id, validated_acc_id, validated_rg_id,
amodule.params['ipcidr'], amodule.params['ipcidr'],
amodule.params['ext_net_id'], amodule.params['ext_ip_addr'], amodule.params['ext_net_id'], amodule.params['ext_ip_addr'],
amodule.params['annotation']) amodule.params['annotation'])
vins_should_exist = True vins_should_exist = True
elif amodule.params['state'] == 'disabled': elif amodule.params['state'] == 'disabled':
decon.result['failed'] = True decon.result['failed'] = True
decon.result['changed'] = False decon.result['changed'] = False
decon.result['msg'] = ("Invalid target state '{}' requested for non-existent " decon.result['msg'] = ("Invalid target state '{}' requested for non-existent "
"ViNS name '{}'").format(amodule.params['state'], "ViNS name '{}'").format(amodule.params['state'],
amodule.params['vins_name']) amodule.params['vins_name'])
# #
# conditional switch end - complete module run # conditional switch end - complete module run
# #

@ -40,6 +40,7 @@ import requests
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
# #
# TODO: the following functionality to be implemented and/or tested # TODO: the following functionality to be implemented and/or tested
# 4) workflow callbacks # 4) workflow callbacks
@ -150,7 +151,7 @@ class DecortController(object):
if self.authenticator == "jwt": if self.authenticator == "jwt":
# validate supplied JWT on the DECORT controller # validate supplied JWT on the DECORT controller
self.validate_jwt() # this call will abort the script if validation fails self.validate_jwt() # this call will abort the script if validation fails
jwt_decoded = jwt.decode(self.jwt, verify=False) jwt_decoded = jwt.decode(self.jwt, algorithms=["ES384"], options={"verify_signature": False})
self.decort_username = jwt_decoded['username'] + "@" + jwt_decoded['iss'] self.decort_username = jwt_decoded['username'] + "@" + jwt_decoded['iss']
elif self.authenticator == "legacy": elif self.authenticator == "legacy":
# obtain session id from the DECORT controller and thus validate the the legacy user # obtain session id from the DECORT controller and thus validate the the legacy user
@ -161,7 +162,7 @@ class DecortController(object):
# obtain JWT from Oauth2 provider and validate on the DECORT controller # obtain JWT from Oauth2 provider and validate on the DECORT controller
self.obtain_oauth2_jwt() self.obtain_oauth2_jwt()
self.validate_jwt() # this call will abort the script if validation fails self.validate_jwt() # this call will abort the script if validation fails
jwt_decoded = jwt.decode(self.jwt, verify=False) jwt_decoded = jwt.decode(self.jwt, algorithms=["ES384"], options={"verify_signature": False})
self.decort_username = jwt_decoded['username'] + "@" + jwt_decoded['iss'] self.decort_username = jwt_decoded['username'] + "@" + jwt_decoded['iss']
# self.run_phase = "Initializing DecortController instance complete." # self.run_phase = "Initializing DecortController instance complete."
@ -205,7 +206,7 @@ class DecortController(object):
client_id=self.app_id, client_id=self.app_id,
client_secret=self.app_secret, client_secret=self.app_secret,
response_type="id_token", response_type="id_token",
validity=3600,) validity=3600, )
# TODO: Need standard code snippet to handle server timeouts gracefully # TODO: Need standard code snippet to handle server timeouts gracefully
# Consider a few retries before giving up or use requests.Session & requests.HTTPAdapter # 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 # see https://stackoverflow.com/questions/15431044/can-i-set-max-retries-for-requests-request
@ -273,7 +274,7 @@ class DecortController(object):
return False return False
req_url = self.controller_url + "/restmachine/cloudapi/account/list" req_url = self.controller_url + "/restmachine/cloudapi/account/list"
req_header = dict(Authorization="bearer {}".format(arg_jwt),) req_header = dict(Authorization="bearer {}".format(arg_jwt), )
try: try:
api_resp = requests.post(req_url, headers=req_header, verify=self.verify_ssl) api_resp = requests.post(req_url, headers=req_header, verify=self.verify_ssl)
@ -318,7 +319,7 @@ class DecortController(object):
req_url = self.controller_url + "/restmachine/cloudapi/user/authenticate" req_url = self.controller_url + "/restmachine/cloudapi/user/authenticate"
req_data = dict(username=self.user, req_data = dict(username=self.user,
password=self.password,) password=self.password, )
try: try:
api_resp = requests.post(req_url, data=req_data, verify=self.verify_ssl) api_resp = requests.post(req_url, data=req_data, verify=self.verify_ssl)
@ -388,7 +389,8 @@ class DecortController(object):
return None # actually, this directive will never be executed as fail_json aborts the script return None # actually, this directive will never be executed as fail_json aborts the script
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
self.result['failed'] = True self.result['failed'] = True
self.result['msg'] = "Timeout when trying to connect to '{}' when calling DECORT API.".format(api_resp.url) self.result['msg'] = "Timeout when trying to connect to '{}' when calling DECORT API.".format(
api_resp.url)
self.amodule.fail_json(**self.result) self.amodule.fail_json(**self.result)
return None return None
@ -536,22 +538,22 @@ class DecortController(object):
if disk['id'] not in new_data_disks: if disk['id'] not in new_data_disks:
detach_list.append(disk['id']) detach_list.append(disk['id'])
attach_list = [ did for did in new_data_disks if did not in current_list ] attach_list = [did for did in new_data_disks if did not in current_list]
for did in detach_list: for did in detach_list:
api_params = dict(computeId = comp_dict['id'], diskId=did) api_params = dict(computeId=comp_dict['id'], diskId=did)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/diskDetach", api_params) self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/diskDetach", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json. # On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['changed'] = True self.result['changed'] = True
for did in attach_list: for did in attach_list:
api_params = dict(computeId = comp_dict['id'], diskId=did) api_params = dict(computeId=comp_dict['id'], diskId=did)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/diskAttach", api_params) self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/diskAttach", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json. # On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['changed'] = True self.result['changed'] = True
self.result['failed'] = False self.result['failed'] = False
return return
def compute_delete(self, comp_id, permanently=False): def compute_delete(self, comp_id, permanently=False):
@ -572,7 +574,7 @@ class DecortController(object):
return return
api_params = dict(computeId=comp_id, api_params = dict(computeId=comp_id,
permanently=permanently,) permanently=permanently, )
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/delete", api_params) 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. # On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False self.result['failed'] = False
@ -593,7 +595,7 @@ class DecortController(object):
ret_comp_dict = None ret_comp_dict = None
ret_rg_id = 0 ret_rg_id = 0
api_params = dict(computeId=comp_id,) api_params = dict(computeId=comp_id, )
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/get", api_params) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/get", api_params)
if api_resp.status_code == 200: if api_resp.status_code == 200:
ret_comp_id = comp_id ret_comp_id = comp_id
@ -605,7 +607,6 @@ class DecortController(object):
return ret_comp_id, ret_comp_dict, ret_rg_id return ret_comp_id, ret_comp_dict, ret_rg_id
def compute_find(self, comp_id, def compute_find(self, comp_id,
comp_name="", rg_id=0, comp_name="", rg_id=0,
check_state=True): check_state=True):
@ -652,21 +653,22 @@ class DecortController(object):
# Therefore, RG ID cannot be zero and compute name cannot be empty. # Therefore, RG ID cannot be zero and compute name cannot be empty.
if not rg_id and comp_name == "": if not rg_id and comp_name == "":
self.result['failed'] = True self.result['failed'] = True
self.result['msg'] = "compute_find(): cannot find Compute by name when either name is empty or RG ID is zero." 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) self.amodule.fail_json(**self.result)
# fail the module - exit # fail the module - exit
api_params = dict(includedeleted=True,) api_params = dict(includedeleted=True, )
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/list", api_params) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/list", api_params)
if api_resp.status_code == 200: if api_resp.status_code == 200:
comp_list = json.loads(api_resp.content.decode('utf8')) comp_list = json.loads(api_resp.content.decode('utf8'))
else: else:
self.result['failed'] = True self.result['failed'] = True
self.result['msg'] = ("compute_find(): failed to get list Computes. HTTP code {}, " self.result['msg'] = ("compute_find(): failed to get list Computes. HTTP code {}, "
"response {}.").format(api_resp.status_code, api_resp.reason) "response {}.").format(api_resp.status_code, api_resp.reason)
self.amodule.fail_json(**self.result) self.amodule.fail_json(**self.result)
# fail the module - exit # fail the module - exit
# if we have validated RG ID at this point, look up Compute by name in this RG # 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 # 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. # destroyed. So we may see here computes in "active" and DELETED states.
@ -753,11 +755,11 @@ class DecortController(object):
self.result['failed'] = False self.result['failed'] = False
self.result['warning'] = ("compute_powerstate(): no power state change required for Compute ID {} from its " self.result['warning'] = ("compute_powerstate(): no power state change required for Compute ID {} from its "
"current state '{}' to desired state '{}'.").format(comp_facts['id'], "current state '{}' to desired state '{}'.").format(comp_facts['id'],
comp_facts['techStatus'], comp_facts['techStatus'],
target_state) target_state)
return return
def kvmvm_provision(self, rg_id, def kvmvm_provision(self, rg_id,
comp_name, arch, comp_name, arch,
cpu, ram, cpu, ram,
boot_disk, image_id, boot_disk, image_id,
@ -791,7 +793,7 @@ class DecortController(object):
"was requested.").format(comp_name, rg_id) "was requested.").format(comp_name, rg_id)
return 0 return 0
api_url="" api_url = ""
if arch == "X86_64": if arch == "X86_64":
api_url = "/restmachine/cloudapi/kvmx86/create" api_url = "/restmachine/cloudapi/kvmx86/create"
elif arch == "PPC64_LE": elif arch == "PPC64_LE":
@ -808,7 +810,7 @@ class DecortController(object):
imageId=image_id, imageId=image_id,
bootDisk=boot_disk, bootDisk=boot_disk,
start=start_on_create, # start_machine parameter requires DECORT API ver 3.3.1 or higher start=start_on_create, # start_machine parameter requires DECORT API ver 3.3.1 or higher
netType="NONE") # we create VM without any network connections netType="NONE") # we create VM without any network connections
if userdata: if userdata:
api_params['userdata'] = json.dumps(userdata) # we need to pass a string object as "userdata" api_params['userdata'] = json.dumps(userdata) # we need to pass a string object as "userdata"
@ -864,7 +866,7 @@ class DecortController(object):
for repair in new_networks: for repair in new_networks:
repair['id'] = int(repair['id']) repair['id'] = int(repair['id'])
api_params = dict(accountId = comp_dict['accountId']) api_params = dict(accountId=comp_dict['accountId'])
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/search", api_params) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/search", api_params)
vins_list = json.loads(api_resp.content.decode('utf8')) vins_list = json.loads(api_resp.content.decode('utf8'))
# #
@ -879,7 +881,8 @@ class DecortController(object):
# return # return
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/extnet/list", api_params) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/extnet/list", api_params)
extnet_list = json.loads(api_resp.content.decode('utf8')) # list of dicts: "name" holds "NET_ADDR/NETMASK", "id" is ID extnet_list = json.loads(
api_resp.content.decode('utf8')) # list of dicts: "name" holds "NET_ADDR/NETMASK", "id" is ID
# #
# Empty extnet_list does not constitute error condition, so we should not fail the module in # Empty extnet_list does not constitute error condition, so we should not fail the module in
# this case. Therefore the following code fragment is commented out. # this case. Therefore the following code fragment is commented out.
@ -891,15 +894,15 @@ class DecortController(object):
# return # return
# Prepare the lists of network interfaces for the compute instance: # Prepare the lists of network interfaces for the compute instance:
vins_iface_list = [] # will contain dict(id=<int>, ipAddress=<str>, mac=<str>) for ifaces connected to ViNS(es) vins_iface_list = [] # will contain dict(id=<int>, ipAddress=<str>, mac=<str>) for ifaces connected to ViNS(es)
enet_iface_list = [] # will contain dict(id=<int>, ipAddress=<str>, mac=<str>) for ifaces connected to Ext net(s) enet_iface_list = [] # will contain dict(id=<int>, ipAddress=<str>, mac=<str>) for ifaces connected to Ext net(s)
for iface in comp_dict['interfaces']: for iface in comp_dict['interfaces']:
if iface['connType'] == 'VXLAN': if iface['connType'] == 'VXLAN':
for vrunner in vins_list: for vrunner in vins_list:
if vrunner['vxlanId'] == iface['connId']: if vrunner['vxlanId'] == iface['connId']:
iface_data = dict(id=vrunner['id'], iface_data = dict(id=vrunner['id'],
ipAddress=iface['ipAddress'], ipAddress=iface['ipAddress'],
mac=iface['mac']) mac=iface['mac'])
vins_iface_list.append(iface_data) vins_iface_list.append(iface_data)
elif iface['connType'] == 'VLAN': elif iface['connType'] == 'VLAN':
ip_addr = netaddr.IPAddress(iface['ipAddress']) ip_addr = netaddr.IPAddress(iface['ipAddress'])
@ -910,8 +913,8 @@ class DecortController(object):
ip_extnet = netaddr.IPNetwork(erunner['ipcidr']) ip_extnet = netaddr.IPNetwork(erunner['ipcidr'])
if ip_addr.value >= ip_extnet.first and ip_addr.value <= ip_extnet.last: if ip_addr.value >= ip_extnet.first and ip_addr.value <= ip_extnet.last:
iface_data = dict(id=erunner['id'], iface_data = dict(id=erunner['id'],
ipAddress=iface['ipAddress'], ipAddress=iface['ipAddress'],
mac=iface['mac']) mac=iface['mac'])
enet_iface_list.append(iface_data) enet_iface_list.append(iface_data)
# If at this point compt_dict["interfaces"] lists some interfaces, but neither vins_iface_list # If at this point compt_dict["interfaces"] lists some interfaces, but neither vins_iface_list
@ -921,55 +924,55 @@ class DecortController(object):
if len(comp_dict['interfaces']) and (not len(vins_iface_list) and not len(enet_iface_list)): if len(comp_dict['interfaces']) and (not len(vins_iface_list) and not len(enet_iface_list)):
self.result['failed'] = True self.result['failed'] = True
self.result['msg'] = ("compute_networks() no match between {} interface(s) of Compute ID {}" self.result['msg'] = ("compute_networks() no match between {} interface(s) of Compute ID {}"
"and available {} ViNS(es) or {} ExtNet(s).").format(len(comp_dict['interfaces']), "and available {} ViNS(es) or {} ExtNet(s).").format(len(comp_dict['interfaces']),
comp_dict['id'], comp_dict['id'],
len(vins_list), len(vins_list),
len(extnet_list)) len(extnet_list))
return return
vins_id_list = [ rec['id'] for rec in vins_iface_list ] vins_id_list = [rec['id'] for rec in vins_iface_list]
enet_id_list = [ rec['id'] for rec in enet_iface_list ] enet_id_list = [rec['id'] for rec in enet_iface_list]
# Build attach list by looking for ViNS/Ext net IDs that appear in new_networks, but do not appear in current lists # Build attach list by looking for ViNS/Ext net IDs that appear in new_networks, but do not appear in current lists
attach_list = [] # attach list holds both ViNS and Ext Net attachment specs, as API handles them the same way attach_list = [] # attach list holds both ViNS and Ext Net attachment specs, as API handles them the same way
for netrunner in new_networks: for netrunner in new_networks:
if netrunner['type'] == 'VINS' and ( netrunner['id'] not in vins_id_list ): if netrunner['type'] == 'VINS' and (netrunner['id'] not in vins_id_list):
net2attach = dict(computeId=comp_dict['id'], net2attach = dict(computeId=comp_dict['id'],
netType='VINS', netType='VINS',
netId=netrunner['id'], netId=netrunner['id'],
ipAddr=netrunner.get('ip_addr', "")) ipAddr=netrunner.get('ip_addr', ""))
attach_list.append(net2attach) attach_list.append(net2attach)
elif netrunner['type'] == 'EXTNET' and ( netrunner['id'] not in enet_id_list ): elif netrunner['type'] == 'EXTNET' and (netrunner['id'] not in enet_id_list):
net2attach = dict(computeId=comp_dict['id'], net2attach = dict(computeId=comp_dict['id'],
netType='EXTNET', netType='EXTNET',
netId=netrunner['id'], netId=netrunner['id'],
ipAddr=netrunner.get('ip_addr', "")) ipAddr=netrunner.get('ip_addr', ""))
attach_list.append(net2attach) attach_list.append(net2attach)
# detach is meaningful only if compute's interfaces list was not empty # detach is meaningful only if compute's interfaces list was not empty
if vins_id_list or enet_id_list: if vins_id_list or enet_id_list:
# Build detach list by looking for ViNS/Ext net IDs that appear in current lists, but do not appear in new_networks # Build detach list by looking for ViNS/Ext net IDs that appear in current lists, but do not appear in new_networks
detach_list = [] # detach list holds both ViNS and Ext Net detachment specs, as API handles them the same way detach_list = [] # detach list holds both ViNS and Ext Net detachment specs, as API handles them the same way
target_list = [ rec['id'] for rec in new_networks if rec['type'] == 'VINS' ] target_list = [rec['id'] for rec in new_networks if rec['type'] == 'VINS']
for netrunner in vins_iface_list: for netrunner in vins_iface_list:
if netrunner['id'] not in target_list: if netrunner['id'] not in target_list:
net2detach = dict(computeId=comp_dict['id'], net2detach = dict(computeId=comp_dict['id'],
ipAddr=netrunner['ipAddress'], ipAddr=netrunner['ipAddress'],
mac=netrunner['mac']) mac=netrunner['mac'])
detach_list.append(net2detach) detach_list.append(net2detach)
target_list = [ rec['id'] for rec in new_networks if rec['type'] == 'EXTNET' ] target_list = [rec['id'] for rec in new_networks if rec['type'] == 'EXTNET']
for netrunner in enet_iface_list: for netrunner in enet_iface_list:
if netrunner['id'] not in target_list: if netrunner['id'] not in target_list:
net2detach = dict(computeId=comp_dict['id'], net2detach = dict(computeId=comp_dict['id'],
ipAddr=netrunner['ipAddress'], ipAddr=netrunner['ipAddress'],
mac=netrunner['mac']) mac=netrunner['mac'])
detach_list.append(net2detach) detach_list.append(net2detach)
for api_params in detach_list: for api_params in detach_list:
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/netDetach", api_params) self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/netDetach", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json. # On success the above call will return here. On error it will abort execution by calling fail_json.
@ -1093,8 +1096,8 @@ class DecortController(object):
if not new_cpu and not new_ram: if not new_cpu and not new_ram:
# if both are 0 or Null - return immediately, as user did not mean to manage size # if both are 0 or Null - return immediately, as user did not mean to manage size
self.result['failed'] = False self.result['failed'] = False
self.result['warning'] =("compute_resize: new CPU count and RAM size are both zero for Compute ID {}" self.result['warning'] = ("compute_resize: new CPU count and RAM size are both zero for Compute ID {}"
" - nothing to do.").format(comp_dict['id']) " - nothing to do.").format(comp_dict['id'])
return return
if not new_cpu: if not new_cpu:
@ -1104,17 +1107,17 @@ class DecortController(object):
# stupid hack? # stupid hack?
if new_ram > 1 and new_ram < 512: if new_ram > 1 and new_ram < 512:
new_ram = new_ram*1024 new_ram = new_ram * 1024
if comp_dict['cpus'] == new_cpu and comp_dict['ram'] == new_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 # no need to call API in this case, as requested size is not different from the current one
self.result['failed'] = False self.result['failed'] = False
self.result['warning'] =("compute_resize: new CPU count and RAM size are the same for Compute ID {}" self.result['warning'] = ("compute_resize: new CPU count and RAM size are the same for Compute ID {}"
" - nothing to do.").format(comp_dict['id']) " - nothing to do.").format(comp_dict['id'])
return return
if ((comp_dict['cpus'] > new_cpu or comp_dict['ram'] > new_ram) and if ((comp_dict['cpus'] > new_cpu or comp_dict['ram'] > new_ram) and
comp_dict['status'] in INVALID_STATES_FOR_HOT_DOWNSIZE): comp_dict['status'] in INVALID_STATES_FOR_HOT_DOWNSIZE):
while wait_for_state_change: while wait_for_state_change:
time.sleep(5) time.sleep(5)
fresh_comp_dict = self.compute_find(arg_vm_id=comp_dict['id']) fresh_comp_dict = self.compute_find(arg_vm_id=comp_dict['id'])
@ -1125,15 +1128,15 @@ class DecortController(object):
if not wait_for_state_change: if not wait_for_state_change:
self.result['failed'] = True self.result['failed'] = True
self.result['msg'] = ("compute_resize(): downsize of Compute ID {} from CPU:RAM {}:{} to {}:{} was " self.result['msg'] = ("compute_resize(): downsize of Compute ID {} from CPU:RAM {}:{} to {}:{} was "
"requested, but its current state '{}' is incompatible with downsize operation.").\ "requested, but its current state '{}' is incompatible with downsize operation."). \
format(comp_dict['id'], format(comp_dict['id'],
comp_dict['cpus'], comp_dict['ram'], comp_dict['cpus'], comp_dict['ram'],
new_cpu, new_ram, comp_dict['status']) new_cpu, new_ram, comp_dict['status'])
return return
api_params = dict(computeId=comp_dict['id'], api_params = dict(computeId=comp_dict['id'],
ram=new_ram, ram=new_ram,
cpu=new_cpu,) cpu=new_cpu, )
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/resize", api_params) 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. # On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False self.result['failed'] = False
@ -1200,7 +1203,6 @@ class DecortController(object):
return image_id, ret_image_dict return image_id, ret_image_dict
def image_find(self, image_id, image_name, account_id, rg_id=0, sepid=0, pool=""): def image_find(self, image_id, image_name, account_id, rg_id=0, sepid=0, pool=""):
"""Locates image specified by name and returns its facts as dictionary. """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, Primary use of this function is to obtain the ID of the image identified by its name and,
@ -1228,10 +1230,10 @@ class DecortController(object):
if image_id > 0: if image_id > 0:
ret_image_id, ret_image_dict = self._image_get_by_id(image_id) ret_image_id, ret_image_dict = self._image_get_by_id(image_id)
if ( ret_image_id and if (ret_image_id and
(sepid == 0 or sepid == ret_image_dict['sepId']) and (sepid == 0 or sepid == ret_image_dict['sepId']) and
(pool == "" or pool == ret_image_dict['pool']) ): (pool == "" or pool == ret_image_dict['pool'])):
return ret_image_id, ret_image_dict return ret_image_id, ret_image_dict
else: else:
validated_acc_id = account_id validated_acc_id = account_id
if account_id == 0: if account_id == 0:
@ -1263,10 +1265,10 @@ class DecortController(object):
self.result['failed'] = True self.result['failed'] = True
self.result['msg'] = ("Failed to find OS image by name '{}', SEP ID {}, pool '{}' for " self.result['msg'] = ("Failed to find OS image by name '{}', SEP ID {}, pool '{}' for "
"account ID '{}'.").format(image_name, "account ID '{}'.").format(image_name,
sepid, pool, sepid, pool,
account_id) account_id)
return 0, None return 0, None
################################### ###################################
# Resource Group (RG) manipulation methods # Resource Group (RG) manipulation methods
################################### ###################################
@ -1292,7 +1294,7 @@ class DecortController(object):
api_params = dict(rgId=rg_id, api_params = dict(rgId=rg_id,
# force=True | False, # force=True | False,
permanently=permanently,) permanently=permanently, )
self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/delete", api_params) 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. # On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False self.result['failed'] = False
@ -1317,7 +1319,7 @@ class DecortController(object):
self.result['msg'] = "rg_get_by_id(): zero RG ID specified." self.result['msg'] = "rg_get_by_id(): zero RG ID specified."
self.amodule.fail_json(**self.result) self.amodule.fail_json(**self.result)
api_params = dict(rgId=rg_id,) api_params = dict(rgId=rg_id, )
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/get", api_params) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/get", api_params)
if api_resp.status_code == 200: if api_resp.status_code == 200:
ret_rg_id = rg_id ret_rg_id = rg_id
@ -1381,7 +1383,7 @@ class DecortController(object):
# try to locate RG by name - start with getting 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_params['accountId'] = arg_account_id
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/account/listRG", api_params) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/account/listRG", api_params)
if api_resp.status_code == 200: if api_resp.status_code == 200:
account_specs = json.loads(api_resp.content.decode('utf8')) account_specs = json.loads(api_resp.content.decode('utf8'))
api_params.pop('accountId') api_params.pop('accountId')
for rg_item in account_specs: for rg_item in account_specs:
@ -1402,7 +1404,6 @@ class DecortController(object):
return ret_rg_id, ret_rg_dict return ret_rg_id, ret_rg_dict
def rg_provision(self, arg_account_id, arg_rg_name, arg_username, arg_quota={}, arg_location="", arg_desc=""): def rg_provision(self, arg_account_id, arg_rg_name, arg_username, arg_quota={}, arg_location="", arg_desc=""):
"""Provision new RG according to the specified arguments. """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 If critical error occurs the embedded call to API function will abort further execution of the script
@ -1434,7 +1435,8 @@ class DecortController(object):
target_gid = self.gid_get(arg_location) target_gid = self.gid_get(arg_location)
if not target_gid: if not target_gid:
self.result['failed'] = True self.result['failed'] = True
self.result['msg'] = ("rg_provision() failed to obtain valid Grid ID for location '{}'").format(arg_location) self.result['msg'] = ("rg_provision() failed to obtain valid Grid ID for location '{}'").format(
arg_location)
self.amodule.fail_json(**self.result) self.amodule.fail_json(**self.result)
api_params = dict(accountId=arg_account_id, api_params = dict(accountId=arg_account_id,
@ -1454,7 +1456,7 @@ class DecortController(object):
api_params['maxCPUCapacity'] = arg_quota['cpu'] api_params['maxCPUCapacity'] = arg_quota['cpu']
if 'ext_ips' in arg_quota: if 'ext_ips' in arg_quota:
api_params['maxNumPublicIP'] = arg_quota['ext_ips'] api_params['maxNumPublicIP'] = arg_quota['ext_ips']
if arg_desc: if arg_desc:
api_params['desc'] = arg_desc api_params['desc'] = arg_desc
@ -1495,12 +1497,12 @@ class DecortController(object):
query_key_map = dict(cpu='CU_C', query_key_map = dict(cpu='CU_C',
ram='CU_M', ram='CU_M',
disk='CU_D', disk='CU_D',
ext_ips='CU_I',) ext_ips='CU_I', )
set_key_map = dict(cpu='maxCPUCapacity', set_key_map = dict(cpu='maxCPUCapacity',
ram='maxMemoryCapacity', ram='maxMemoryCapacity',
disk='maxVDiskCapacity', disk='maxVDiskCapacity',
ext_ips='maxNumPublicIP',) ext_ips='maxNumPublicIP', )
api_params = dict(rgId=arg_rg_dict['id'],) api_params = dict(rgId=arg_rg_dict['id'], )
quota_change_required = False quota_change_required = False
for new_limit in ('cpu', 'ram', 'disk', 'ext_ips'): for new_limit in ('cpu', 'ram', 'disk', 'ext_ips'):
@ -1543,7 +1545,7 @@ class DecortController(object):
return return
api_params = dict(rgId=arg_rg_id, api_params = dict(rgId=arg_rg_id,
reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username),) reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username), )
self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/restore", api_params) 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. # On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False self.result['failed'] = False
@ -1560,7 +1562,8 @@ class DecortController(object):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_state") self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "rg_state")
NOP_STATES_FOR_RG_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING", "DESTROYED"] NOP_STATES_FOR_RG_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING",
"DESTROYED"]
VALID_TARGET_STATES = ["enabled", "disabled"] VALID_TARGET_STATES = ["enabled", "disabled"]
if arg_rg_dict['status'] in NOP_STATES_FOR_RG_CHANGE: if arg_rg_dict['status'] in NOP_STATES_FOR_RG_CHANGE:
@ -1573,7 +1576,7 @@ class DecortController(object):
self.result['failed'] = False self.result['failed'] = False
self.result['warning'] = ("rg_state(): unrecognized desired state '{}' requested " self.result['warning'] = ("rg_state(): unrecognized desired state '{}' requested "
"for RG ID {}. No RG state change will be done.").format(arg_desired_state, "for RG ID {}. No RG state change will be done.").format(arg_desired_state,
arg_rg_dict['id']) arg_rg_dict['id'])
return return
if self.amodule.check_mode: if self.amodule.check_mode:
@ -1677,7 +1680,7 @@ class DecortController(object):
"requested.").format(arg_type, arg_mode, arg_vmid) "requested.").format(arg_type, arg_mode, arg_vmid)
return 0 return 0
api_params=dict( api_params = dict(
machineId=arg_vmid, machineId=arg_vmid,
gpu_type=arg_type, gpu_type=arg_type,
gpu_mode=arg_mode, gpu_mode=arg_mode,
@ -1709,7 +1712,7 @@ class DecortController(object):
"requested.").format(arg_vgpuid, arg_vmid) "requested.").format(arg_vgpuid, arg_vmid)
return True return True
api_params=dict( api_params = dict(
machineId=arg_vmid, machineId=arg_vmid,
vgpuid=arg_vgpuid, vgpuid=arg_vgpuid,
) )
@ -1734,7 +1737,7 @@ class DecortController(object):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "gpu_list") self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "gpu_list")
api_params=dict( api_params = dict(
machineId=arg_vmid, machineId=arg_vmid,
list_destroyed=arg_list_destroyed, list_destroyed=arg_list_destroyed,
) )
@ -1749,7 +1752,7 @@ class DecortController(object):
ret_gpu_list = json.loads(api_resp.content.decode('utf8')) ret_gpu_list = json.loads(api_resp.content.decode('utf8'))
return ret_gpu_list return ret_gpu_list
################################### ###################################
# Workflow callback stub methods - not fully implemented yet # Workflow callback stub methods - not fully implemented yet
################################### ###################################
@ -1798,7 +1801,7 @@ class DecortController(object):
ret_gid = 0 ret_gid = 0
api_params = dict() api_params = dict()
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/locations/list", api_params) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/locations/list", api_params)
if api_resp.status_code == 200: if api_resp.status_code == 200:
locations = json.loads(api_resp.content.decode('utf8')) locations = json.loads(api_resp.content.decode('utf8'))
if location_code == "" and locations: if location_code == "" and locations:
ret_gid = locations[0]['gid'] ret_gid = locations[0]['gid']
@ -1839,7 +1842,7 @@ class DecortController(object):
api_params = dict(vinsId=vins_id, api_params = dict(vinsId=vins_id,
# force=True | False, # force=True | False,
permanently=permanently,) permanently=permanently, )
self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/delete", api_params) 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. # On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False self.result['failed'] = False
@ -1867,7 +1870,7 @@ class DecortController(object):
self.result['msg'] = "vins_get_by_id(): zero ViNS ID specified." self.result['msg'] = "vins_get_by_id(): zero ViNS ID specified."
self.amodule.fail_json(**self.result) self.amodule.fail_json(**self.result)
api_params = dict(vinsId=vins_id,) api_params = dict(vinsId=vins_id, )
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/get", api_params) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/get", api_params)
if api_resp.status_code == 200: if api_resp.status_code == 200:
ret_vins_id = vins_id ret_vins_id = vins_id
@ -1949,12 +1952,12 @@ class DecortController(object):
return ret_vins_id, ret_vins_facts return ret_vins_id, ret_vins_facts
else: else:
return 0, None return 0, None
else: # both Account ID and RG ID are zero - fail the module else: # both Account ID and RG ID are zero - fail the module
self.result['failed'] = True self.result['failed'] = True
self.result['msg'] = ("vins_find(): cannot find ViNS by name '{}' " self.result['msg'] = ("vins_find(): cannot find ViNS by name '{}' "
"when no account ID or RG ID is specified.").format(vins_name) "when no account ID or RG ID is specified.").format(vins_name)
self.amodule.fail_json(**self.result) self.amodule.fail_json(**self.result)
else: # ViNS ID is 0 and ViNS name is emtpy - fail the module else: # ViNS ID is 0 and ViNS name is emtpy - fail the module
self.result['failed'] = True self.result['failed'] = True
self.result['msg'] = "vins_find(): cannot find ViNS by zero ID and empty name." self.result['msg'] = "vins_find(): cannot find ViNS by zero ID and empty name."
self.amodule.fail_json(**self.result) self.amodule.fail_json(**self.result)
@ -2055,7 +2058,7 @@ class DecortController(object):
return return
api_params = dict(vinsId=vins_id, api_params = dict(vinsId=vins_id,
reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username),) 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) 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. # On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False self.result['failed'] = False
@ -2072,7 +2075,8 @@ class DecortController(object):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_state") self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_state")
NOP_STATES_FOR_VINS_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING", "DESTROYED"] NOP_STATES_FOR_VINS_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING",
"DESTROYED"]
VALID_TARGET_STATES = ["enabled", "disabled"] VALID_TARGET_STATES = ["enabled", "disabled"]
if vins_dict['status'] in NOP_STATES_FOR_VINS_CHANGE: if vins_dict['status'] in NOP_STATES_FOR_VINS_CHANGE:
@ -2085,13 +2089,13 @@ class DecortController(object):
self.result['failed'] = False self.result['failed'] = False
self.result['warning'] = ("vins_state(): unrecognized desired state '{}' requested " self.result['warning'] = ("vins_state(): unrecognized desired state '{}' requested "
"for ViNS ID {}. No ViNS state change will be done.").format(desired_state, "for ViNS ID {}. No ViNS state change will be done.").format(desired_state,
vins_dict['id']) vins_dict['id'])
return return
if self.amodule.check_mode: if self.amodule.check_mode:
self.result['failed'] = False self.result['failed'] = False
self.result['msg'] = ("vins_state() in check mode: setting state of ViNS ID {}, name '{}' to " self.result['msg'] = ("vins_state() in check mode: setting state of ViNS ID {}, name '{}' to "
"'{}' was requested.").format(vins_dict['id'],vins_dict['name'], "'{}' was requested.").format(vins_dict['id'], vins_dict['name'],
desired_state) desired_state)
return return
@ -2137,14 +2141,14 @@ class DecortController(object):
recommended to update ViNS facts in the upstream code. recommended to update ViNS facts in the upstream code.
""" """
api_params = dict(vinsId=vins_dict['id'],) api_params = dict(vinsId=vins_dict['id'], )
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_update") self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_update")
if self.amodule.check_mode: if self.amodule.check_mode:
self.result['failed'] = False self.result['failed'] = False
self.result['msg'] = ("vins_update() in check mode: updating ViNS ID {}, name '{}' " self.result['msg'] = ("vins_update() in check mode: updating ViNS ID {}, name '{}' "
"was requested.").format(vins_dict['id'],vins_dict['name']) "was requested.").format(vins_dict['id'], vins_dict['name'])
return return
if not vins_dict['rgId']: if not vins_dict['rgId']:
@ -2184,7 +2188,7 @@ class DecortController(object):
self.result['warning'] = ("vins_update(): ViNS ID {} is already connected to ext net ID {}, " 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'], "ignore ext IP address change if any.").format(vins_dict['id'],
ext_net_id) ext_net_id)
else: # ext_net_id = 0, i.e. connect ViNS to default network 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 # we will connect ViNS to default network only if it is NOT connected to any ext network yet
if not gw_config: if not gw_config:
api_params['netId'] = 0 api_params['netId'] = 0
@ -2195,7 +2199,8 @@ class DecortController(object):
self.result['failed'] = False self.result['failed'] = False
self.result['warning'] = ("vins_update(): ViNS ID {} is already connected to ext net ID {}, " 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'], "no reconnection to default network will be done.").format(vins_dict['id'],
gw_config['ext_net_id']) gw_config[
'ext_net_id'])
return return
@ -2223,7 +2228,7 @@ class DecortController(object):
api_params = dict(diskId=disk_id, api_params = dict(diskId=disk_id,
detach=force_detach, detach=force_detach,
permanently=permanently,) permanently=permanently, )
self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/delete", api_params) 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. # On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False self.result['failed'] = False
@ -2251,7 +2256,7 @@ class DecortController(object):
self.result['msg'] = "disk_get_by_id(): zero Disk ID specified." self.result['msg'] = "disk_get_by_id(): zero Disk ID specified."
self.amodule.fail_json(**self.result) self.amodule.fail_json(**self.result)
api_params = dict(diskId=disk_id,) api_params = dict(diskId=disk_id, )
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/get", api_params) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/get", api_params)
if api_resp.status_code == 200: if api_resp.status_code == 200:
ret_disk_id = disk_id ret_disk_id = disk_id
@ -2278,14 +2283,15 @@ class DecortController(object):
if no Disk found and check_state=False, so make sure to check return values in the upstream if no Disk found and check_state=False, so make sure to check return values in the upstream
code accordingly. code accordingly.
""" """
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_find") self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "disk_find")
DISK_INVALID_STATES = ["MODELED", "CREATING", "DELETING", "DESTROYING"] DISK_INVALID_STATES = ["MODELED", "CREATING", "DELETING", "DESTROYING"]
if self.amodule.check_mode: if self.amodule.check_mode:
self.result['failed'] = False self.result['failed'] = False
self.result['msg'] = "disk_find() in check mode: find Disk ID {} / name '{}' was requested.".format(disk_id, disk_name) self.result['msg'] = "disk_find() in check mode: find Disk ID {} / name '{}' was requested.".format(disk_id,
disk_name)
return return
ret_disk_id = 0 ret_disk_id = 0
@ -2314,12 +2320,12 @@ class DecortController(object):
return runner['id'], runner return runner['id'], runner
else: else:
return 0, None return 0, None
else: # we are missing meaningful account_id - fail the module else: # we are missing meaningful account_id - fail the module
self.result['failed'] = True self.result['failed'] = True
self.result['msg'] = ("disk_find(): cannot find Disk by name '{}' " self.result['msg'] = ("disk_find(): cannot find Disk by name '{}' "
"when no account ID specified.").format(disk_name) "when no account ID specified.").format(disk_name)
self.amodule.fail_json(**self.result) self.amodule.fail_json(**self.result)
else: # Disk ID is 0 and Disk name is emtpy - fail the module else: # Disk ID is 0 and Disk name is emtpy - fail the module
self.result['failed'] = True self.result['failed'] = True
self.result['msg'] = "disk_find(): cannot find Disk by zero ID and empty name." self.result['msg'] = "disk_find(): cannot find Disk by zero ID and empty name."
self.amodule.fail_json(**self.result) self.amodule.fail_json(**self.result)
@ -2348,7 +2354,8 @@ class DecortController(object):
if self.amodule.check_mode: if self.amodule.check_mode:
self.result['failed'] = False self.result['failed'] = False
self.result['msg'] = "disk_provision() in check mode: create Disk name '{}' was requested.".format(disk_name) self.result['msg'] = "disk_provision() in check mode: create Disk name '{}' was requested.".format(
disk_name)
return 0 return 0
target_gid = self.gid_get(location) target_gid = self.gid_get(location)
@ -2364,7 +2371,7 @@ class DecortController(object):
desc=desc, desc=desc,
size=size, size=size,
type='D', type='D',
sepId=sep_id,) sepId=sep_id, )
if pool_name != "": if pool_name != "":
api_params['pool'] = pool_name api_params['pool'] = pool_name
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/create", api_params) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/create", api_params)
@ -2400,13 +2407,14 @@ class DecortController(object):
if not new_size: if not new_size:
self.result['failed'] = False self.result['failed'] = False
self.result['warning'] = "disk_resize(): zero size requested for Disk ID {} - ignoring.".format(disk_facts['id']) self.result['warning'] = "disk_resize(): zero size requested for Disk ID {} - ignoring.".format(
disk_facts['id'])
return return
if new_size < disk_facts['sizeMax']: if new_size < disk_facts['sizeMax']:
self.result['failed'] = True self.result['failed'] = True
self.result['msg'] = ("disk_resize(): downsizing Disk ID {} is not allowed - current " self.result['msg'] = ("disk_resize(): downsizing Disk ID {} is not allowed - current "
"size {}, requeste size {}.").format(disk_facts['id'], "size {}, requeste size {}.").format(disk_facts['id'],
disk_facts['sizeMax'], new_size) disk_facts['sizeMax'], new_size)
return return
@ -2444,14 +2452,13 @@ class DecortController(object):
return return
api_params = dict(diskId=disk_id, api_params = dict(diskId=disk_id,
reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username),) reason="Restored on user {} request by DECORT Ansible module.".format(self.decort_username), )
self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/restore", api_params) 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. # On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False self.result['failed'] = False
self.result['changed'] = True self.result['changed'] = True
return return
############################## ##############################
# #
# Port Forward rules management # Port Forward rules management
@ -2519,14 +2526,15 @@ class DecortController(object):
ret_rules = self._pfw_get(comp_facts['id'], vins_facts['id']) ret_rules = self._pfw_get(comp_facts['id'], vins_facts['id'])
return ret_rules return ret_rules
iface_ipaddr = "" # keep IP address associated with Compute's connection to this ViNS - need this for natRuleDel API iface_ipaddr = "" # keep IP address associated with Compute's connection to this ViNS - need this for natRuleDel API
for iface in comp_facts['interfaces']: for iface in comp_facts['interfaces']:
if iface['connType'] == 'VXLAN' and iface['connId'] == vins_facts['vxlanId']: if iface['connType'] == 'VXLAN' and iface['connId'] == vins_facts['vxlanId']:
iface_ipaddr = iface['ipAddress'] iface_ipaddr = iface['ipAddress']
break break
else: else:
decon.result['failed'] = True decon.result['failed'] = True
decon.result['msg'] = "Compute ID {} is not connected to ViNS ID {}.".format(comp_facts['id'], vins_facts['id']) decon.result['msg'] = "Compute ID {} is not connected to ViNS ID {}.".format(comp_facts['id'],
vins_facts['id'])
return ret_rules return ret_rules
existing_rules = [] existing_rules = []
@ -2574,9 +2582,9 @@ class DecortController(object):
rule['local_port'] = rule['public_port_start'] rule['local_port'] = rule['public_port_start']
for runner in existing_rules: for runner in existing_rules:
if (runner['publicPortStart'] == rule['public_port_start'] and if (runner['publicPortStart'] == rule['public_port_start'] and
runner['publicPortEnd'] == rule_port_end and runner['publicPortEnd'] == rule_port_end and
runner['localPort'] == rule['local_port'] and runner['localPort'] == rule['local_port'] and
runner['protocol'] == rule['proto']): runner['protocol'] == rule['proto']):
rule['action'] = 'keep' rule['action'] = 'keep'
break break
if rule['action'] == 'add': if rule['action'] == 'add':
@ -2594,9 +2602,9 @@ class DecortController(object):
for runner in new_rules: for runner in new_rules:
runner_port_end = runner.get('public_port_end', runner['public_port_start']) runner_port_end = runner.get('public_port_end', runner['public_port_start'])
if (rule['publicPortStart'] == runner['public_port_start'] and if (rule['publicPortStart'] == runner['public_port_start'] and
rule['publicPortEnd'] == runner_port_end and rule['publicPortEnd'] == runner_port_end and
rule['localPort'] == runner['local_port'] and rule['localPort'] == runner['local_port'] and
rule['protocol'] == runner['proto']): rule['protocol'] == runner['proto']):
rule['action'] = 'keep' rule['action'] = 'keep'
break break
if rule['action'] == 'del': if rule['action'] == 'del':
@ -2640,4 +2648,257 @@ class DecortController(object):
self.result['failed'] = False self.result['failed'] = False
ret_rules = self._pfw_get(comp_facts['id'], vins_facts['id']) ret_rules = self._pfw_get(comp_facts['id'], vins_facts['id'])
return ret_rules return ret_rules
def _k8s_get_by_id(self, k8s_id):
"""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
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_id = k8s_id
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_id, ret_k8s_dict
def k8s_find(self, arg_k8s_id=0, arg_k8s_name="", arg_check_state=True):
"""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.
@return: ID of the k8s, if found. Zero otherwise.
@return: dictionary with k8s facts if k8s 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 k8s manipulation viewpoint
#
K8S_INVALID_STATES = ["MODELED"]
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_find")
ret_k8s_id = 0
api_params = dict(includedeleted=True)
ret_k8s_dict = None
if arg_k8s_id > 0:
ret_k8s_id, ret_k8s_dict = self._k8s_get_by_id(arg_k8s_id)
if not ret_k8s_id:
self.result['failed'] = True
self.result['msg'] = "k8s_find(): cannot find k8s by ID {}.".format(arg_k8s_id)
self.amodule.fail_json(**self.result)
elif arg_k8s_name != "":
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/list", api_params)
if api_resp.status_code == 200:
account_specs = json.loads(api_resp.content.decode('utf8'))
for k8s_item in account_specs:
got_id, got_specs = self._k8s_get_by_id(k8s_item['id'])
if got_id and got_specs['name'] == arg_k8s_name:
# name matches
if not arg_check_state or got_specs['status'] not in K8S_INVALID_STATES:
ret_k8s_id = got_id
ret_k8s_dict = got_specs
break
# Note: we do not fail the run if k8s cannot be located by its name, because it could be a new k8s
# that never existed before. In this case ret_k8s_id=0 and empty ret_k8s_dict will be returned.
else:
# Both arg_k8s_id and arg_k8s_name are empty - there is no way to locate k8s in this case
self.result['failed'] = True
self.result['msg'] = "k8s_find(): either non-zero ID or a non-empty name must be specified."
self.amodule.fail_json(**self.result)
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"]
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
k8s_state_api = "" # This string will also be used as a flag to indicate that API call is necessary
api_params = dict(k8sId=arg_k8s_dict['id'])
expected_state = ""
tech_state = ""
if arg_k8s_dict['status'] in ["CREATED", "ENABLED"] and arg_desired_state == 'disabled':
k8s_state_api = "/restmachine/cloudapi/k8s/disable"
expected_state = "DISABLED"
elif arg_k8s_dict['status'] in ["CREATED", "DISABLED"] and arg_desired_state == 'enabled':
k8s_state_api = "/restmachine/cloudapi/k8s/enable"
expected_state = "ENABLED"
elif arg_k8s_dict['status'] == "ENABLED" and arg_desired_state == 'enabled' and arg_started is True and arg_k8s_dict['techStatus'] == "STOPPED":
k8s_state_api = "/restmachine/cloudapi/k8s/start"
tech_state = "STARTED"
elif arg_k8s_dict['status'] == "ENABLED" and arg_desired_state == 'enabled' and arg_started is False and arg_k8s_dict['techStatus'] == "STARTED":
k8s_state_api = "/restmachine/cloudapi/k8s/stop"
tech_state = "STOPPED"
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)
return
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
self.result['msg'] = "k8s_delete() in check mode: delete Compute ID {} was requested.".format(k8s_id)
return
api_params = dict(k8sId=k8s_id,
permanently=permanently,
)
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
self.result['changed'] = True
return
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
def k8s_provision(self, k8s_name,
wg_name, k8ci_id,
rg_id, master_count,
master_cpu, master_ram,
master_disk, worker_count,
worker_cpu, worker_ram,
worker_disk, extnet_id,
with_lb, annotation, ):
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
api_url = "/restmachine/cloudapi/k8s/create"
api_params = dict(name=k8s_name,
rgId=rg_id,
k8ciId=k8ci_id,
workerGroupName=wg_name,
masterNum=master_count,
masterCpu=master_cpu,
masterRam=master_ram,
masterDisk=master_disk,
workerNum=worker_count,
workerCpu=worker_cpu,
workerRam=worker_ram,
workerDisk=worker_disk,
extnetId=extnet_id,
withLB=with_lb,
desc=annotation,
)
api_resp = self.decort_api_call(requests.post, api_url, api_params)
k8s_id = ""
if api_resp.status_code == 200:
for i in range(300):
api_get_url = "/restmachine/cloudapi/tasks/get"
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['failed'] = False
time.sleep(30)
elif ret_info['status'] == "ERROR":
self.result['failed'] = True
return
elif ret_info['status'] == "OK":
k8s_id = ret_info['result'][0]
self.result['changed'] = True
return k8s_id
else:
k8s_id = ret_info['status']
else:
self.result['failed'] = True
# Timeout
self.result['failed'] = True
else:
self.result['failed'] = True
return

Loading…
Cancel
Save