11 Commits

Author SHA1 Message Date
Aleksandr Malyavin
587f0d9c0b Update README, add kubernetes support 2022-04-04 17:03:01 +03:00
Filipp Ignatenko
36773c7bb3 Update README.md 2022-03-10 13:05:08 +03:00
Alex_geth
3d9917b8a7 api update, arch update 2022-01-27 13:17:14 +03:00
Sergey Shubin svs1370
a166ce1c8d Update module info for DECORT API 3.6.1 2021-04-21 12:45:30 +03:00
Sergey Shubin svs1370
e81bf1ca16 Initial adaptation for DECORT API ver.3.6.1 2021-04-21 12:29:15 +03:00
Sergey Shubin svs1370
2c95c6ef0c Add extra checks for compute data disks management 2021-04-06 14:49:09 +03:00
Sergey Shubin svs1370
18067b82b7 Fix pool_name argument name in call to provision_disk 2021-04-06 13:32:25 +03:00
Sergey Shubin svs1370
bc317d1438 Change default pool name for disk provisioning to empty string 2021-03-29 10:14:43 +03:00
Sergey Shubin svs1370
e17c8be53a Rework conditions which cause compute_network method to fail module execution 2021-03-25 21:34:53 +03:00
Sergey Shubin svs1370
2014863c37 Fix incorrect condition when listing ext nets in compute_networks 2021-03-25 18:49:04 +03:00
Sergey Shubin svs1370
4b57777a2c Change external network list API URL to use new group extnet 2020-12-21 09:48:12 +03:00
11 changed files with 884 additions and 229 deletions

View File

@@ -1,12 +1,12 @@
# decort-ansible
Ansible modules for Digital Energy Orchestration Technology (DECORT) platform v3.4.2 and above
Ansible modules for Digital Energy Orchestration Technology (DECORT) platform v3.6.1 and above.
Note that this module may produce unreliable results when used with older DECORT API versions.
Requirements:
* Ansible 2.7 or higher
* Python 2.6 or higher
* PyJWT Python module
* Python 3.7 or higher
* PyJWT 2.0.0 Python module or higher
* requests Python module
* netaddr Python module
* DECORT cloud platform version 3.4.2 or higher
* DECORT cloud platform version 3.5.0 or higher

View File

@@ -188,3 +188,136 @@
var: my_pfw.facts
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

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
#
# Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible
# Copyright: (c) 2018-2020 Digital Energy Cloud Solutions LLC
# Copyright: (c) 2018-2021 Digital Energy Cloud Solutions LLC
#
# Apache License 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.txt)
#
@@ -30,7 +30,7 @@ requirements:
- requests Python module
- netaddr Python module
- decort_utils utility library (module)
- DECORT cloud platform version 3.4.2 or higher
- DECORT cloud platform version 3.6.1 or higher
notes:
- Environment variables can be used to pass selected parameters to the module, see details below.
- Specified Oauth2 provider must be trusted by the DECORT cloud controller on which JWT will be used.
@@ -139,7 +139,7 @@ options:
description:
- Name of the pool where to place new disk. Once disk is created, its pool cannot be changed.
- This parameter is used when creating new disk and igonred for all other operations.
default: default
default: empty string
required: no
sep_id:
description:
@@ -209,7 +209,6 @@ EXAMPLES = '''
controller_url: "https://cloud.digitalenergy.online"
name: "MyDataDisk01"
sep_id: 1
pool: "default"
size: 50
account_name: "MyAccount"
state: present
@@ -275,7 +274,7 @@ def decort_disk_package_facts(disk_facts, check_mode=False):
ret_dict['size'] = disk_facts['sizeMax']
ret_dict['state'] = disk_facts['status']
ret_dict['account_id'] = disk_facts['accountId']
ret_dict['sep_id'] = disk_facts['sepid']
ret_dict['sep_id'] = disk_facts['sepId']
ret_dict['pool'] = disk_facts['pool']
ret_dict['attached_to'] = disk_facts['vmid']
ret_dict['gid'] = disk_facts['gid']
@@ -316,7 +315,7 @@ def decort_disk_parameters():
fallback=(env_fallback, ['DECORT_PASSWORD']),
no_log=True),
place_with=dict(type='int', required=False, default=0),
pool=dict(type='str', required=False, default='default'),
pool=dict(type='str', required=False, default=''),
sep_id=dict(type='int', required=False, default=0),
size=dict(type='int', required=False),
state=dict(type='str',
@@ -402,7 +401,7 @@ def main():
disk_should_exist = False
target_sep_id = 0
target_pool = "default"
# target_pool = ""
if disk_id:
disk_should_exist = True
@@ -455,10 +454,10 @@ def main():
# request to place this disk on the same SEP as the specified OS image
# validate specified OS image and assign SEP ID accordingly
image_id, image_facts = decon.image_find(amodule.params['place_with'], "", 0)
target_sep_id = image_facts['sepid']
target_sep_id = image_facts['sepId']
else:
# no new SEP ID is explicitly specified, and no place_with option - use sep_id from the disk_facts
target_sep_id = disk_facts['sepid']
# no new SEP ID is explicitly specified, and no place_with option - use sepId from the disk_facts
target_sep_id = disk_facts['sepId']
disk_id = decon.disk_provision(disk_name=disk_facts['name'], # as this disk was found, its name is in the facts
size=amodule.params['size'],
account_id=validated_acc_id,
@@ -496,7 +495,7 @@ def main():
# request to place this disk on the same SEP as the specified OS image
# validate specified OS image and assign SEP ID accordingly
image_id, image_facts = decon.image_find(amodule.params['place_with'], "", 0)
target_sep_id = image_facts['sepid']
target_sep_id = image_facts['sepId']
else:
# no SEP ID is explicitly specified, and no place_with option - we do not know where
# to place the new disk - fail the module
@@ -509,7 +508,7 @@ def main():
size=amodule.params['size'],
account_id=validated_acc_id,
sep_id=target_sep_id,
pool=amodule.params['pool'],
pool_name=amodule.params['pool'],
desc=amodule.params['annotation'],
location="")
disk_should_exist = True

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
#
# Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible
# Copyright: (c) 2018-2020 Digital Energy Cloud Solutions LLC
# Copyright: (c) 2018-2021 Digital Energy Cloud Solutions LLC
#
# Apache License 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.txt)
#
@@ -32,7 +32,7 @@ requirements:
- PyJWT module
- requests module
- decort_utils utility library (module)
- DECORT cloud platform version 3.4.2 or higher
- DECORT cloud platform version 3.6.1 or higher
options:
app_id:
description:

236
library/decort_k8s.py Normal file
View File

@@ -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()

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
#
# Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible
# Copyright: (c) 2018-2020 Digital Energy Cloud Solutions LLC
# Copyright: (c) 2018-2021 Digital Energy Cloud Solutions LLC
#
# Apache License 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.txt)
#
@@ -32,7 +32,7 @@ requirements:
- requests Python module
- netaddr Python module
- decort_utils utility library (module)
- DECORT cloud platform version 3.4.2 or higher
- DECORT cloud platform version 3.6.1 or higher
notes:
- Environment variables can be used to pass selected parameters to the module, see details below.
- Specified Oauth2 provider must be trusted by the DECORT cloud controller on which JWT will be used.
@@ -77,8 +77,8 @@ options:
- Architecture of the KVM VM. DECORT supports KVM hosts based on Intel x86 and IBM PowerPC hardware.
- This parameter is used when new KVM VM is created and ignored for all other operations.
- Module may fail if your DECORT installation does not have physical nodes of specified architecture.
default: KVM_X86
choices: [ KVM_X86, KVM_PPC ]
default: X86_64
choices: [ X86_64, PPC64_LE ]
required: yes
authenticator:
description:
@@ -439,8 +439,8 @@ class decort_kvmvm(DecortController):
check_state=False)
if self.comp_id:
if self.comp_info['status'] != 'DESTROYED' and self.comp_info['arch'] not in ["KVM_X86", "KVM_PPC"]:
# If we found a Compute in a non-DESTROYED state and it is not of type KVM_*, abort the module
if self.comp_info['status'] != 'DESTROYED' and self.comp_info['arch'] not in ["X86_64", "PPC64_LE"]:
# If we found a Compute in a non-DESTROYED state and it is not of type valid arch, abort the module
self.result['failed'] = True
self.result['msg'] = ("Compute ID {} architecture '{}' is not supported by "
"decort_kvmvm module.").format(self.comp_id,
@@ -500,7 +500,7 @@ class decort_kvmvm(DecortController):
self.check_amodule_argument('cpu')
self.check_amodule_argument('ram')
if self.amodule.params['arch'] not in ["KVM_X86", "KVM_PPC"]:
if self.amodule.params['arch'] not in ["X86_64", "PPC64_LE"]:
self.result['failed'] = True
self.result['msg'] = ("Unsupported architecture '{}' is specified for "
"KVM VM create.").format(self.amodule.params['arch'])
@@ -743,7 +743,7 @@ class decort_kvmvm(DecortController):
required=False,
fallback=(env_fallback, ['DECORT_APP_SECRET']),
no_log=True),
arch=dict(type='str', choices=['KVM_X86', 'KVM_PPC'], default='KVM_X86'),
arch=dict(type='str', choices=['X86_64', 'PPC64_LE'], default='X86_64'),
authenticator=dict(type='str',
required=True,
choices=['legacy', 'oauth2', 'jwt']),

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
#
# Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible
# Copyright: (c) 2018-2020 Digital Energy Cloud Solutions LLC
# Copyright: (c) 2018-2021 Digital Energy Cloud Solutions LLC
#
# Apache License 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.txt)
#
@@ -33,7 +33,7 @@ requirements:
- requests Python module
- netaddr Python module
- decort_utils utility library (module)
- DECORT cloud platform version 3.4.2 or higher.
- DECORT cloud platform version 3.6.1 or higher.
notes:
- Environment variables can be used to pass selected parameters to the module, see details below.
- Specified Oauth2 provider must be trusted by the DECORT cloud controller on which JWT will be used.
@@ -203,7 +203,7 @@ def decort_osimage_package_facts(arg_osimage_facts, arg_check_mode=False):
ret_dict['size'] = arg_osimage_facts['size']
ret_dict['type'] = arg_osimage_facts['type']
# ret_dict['arch'] = arg_osimage_facts['architecture']
ret_dict['sep_id'] = arg_osimage_facts['sepid']
ret_dict['sep_id'] = arg_osimage_facts['sepId']
ret_dict['pool'] = arg_osimage_facts['pool']
ret_dict['state'] = arg_osimage_facts['status']

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
#
# Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible
# Copyright: (c) 2018-2020 Digital Energy Cloud Solutions LLC
# Copyright: (c) 2018-2021 Digital Energy Cloud Solutions LLC
#
# Apache License 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.txt)
#
@@ -30,7 +30,7 @@ requirements:
- requests Python module
- netaddr Python module
- decort_utils utility library (module)
- DECORT cloud platform version 3.4.2 or higher
- DECORT cloud platform version 3.6.1 or higher
notes:
- Environment variables can be used to pass selected parameters to the module, see details below.
- Specified Oauth2 provider must be trusted by the DECORT cloud controller on which JWT will be used.

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
#
# Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible
# Copyright: (c) 2018-2020 Digital Energy Cloud Solutions LLC
# Copyright: (c) 2018-2021 Digital Energy Cloud Solutions LLC
#
# Apache License 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.txt)
#
@@ -30,7 +30,7 @@ requirements:
- requests Python module
- netaddr Python module
- decort_utils utility library (module)
- DECORT cloud platform version 3.4.2 or higher
- DECORT cloud platform version 3.6.1 or higher
notes:
- Environment variables can be used to pass selected parameters to the module, see details below.
- Specified Oauth2 provider must be trusted by the DECORT cloud controller on which JWT will be used.

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
#
# Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible
# Copyright: (c) 2018-2020 Digital Energy Cloud Solutions LLC
# Copyright: (c) 2018-2021 Digital Energy Cloud Solutions LLC
#
# Apache License 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.txt)
#
@@ -30,7 +30,7 @@ requirements:
- requests Python module
- netaddr Python module
- decort_utils utility library (module)
- DECORT cloud platform version 3.4.2 or higher
- DECORT cloud platform version 3.6.1 or higher
notes:
- Environment variables can be used to pass selected parameters to the module, see details below.
- Specified Oauth2 provider must be trusted by the DECORT cloud controller on which JWT will be used.
@@ -273,7 +273,7 @@ def decort_vins_package_facts(arg_vins_facts, arg_check_mode=False):
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['rg_id'] = arg_vins_facts['rgId']
ret_dict['int_net_addr'] = arg_vins_facts['network']
ret_dict['gid'] = arg_vins_facts['gid']
@@ -290,9 +290,9 @@ def decort_vins_package_facts(arg_vins_facts, arg_check_mode=False):
# ??? -> ext_net_id
# tech_status -> techStatus
return ret_dict
def decort_vins_parameters():
"""Build and return a dictionary of parameters expected by decort_vins module in a form accepted
by AnsibleModule utility class."""
@@ -342,6 +342,7 @@ def decort_vins_parameters():
workflow_context=dict(type='str', required=False),
)
# Workflow digest:
# 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
@@ -385,7 +386,7 @@ def main():
decon.fail_json(**decon.result)
vins_level = "ID"
validated_acc_id = vins_facts['accountId']
validated_rg_id = vins_facts['rgid']
validated_rg_id = vins_facts['rgId']
elif amodule.params['rg_id']:
# expect ViNS @ RG level in the RG with specified ID
vins_level = "RG"
@@ -570,7 +571,6 @@ def main():
decon.result['msg'] = ("Invalid target state '{}' requested for non-existent "
"ViNS name '{}'").format(amodule.params['state'],
amodule.params['vins_name'])
#
# conditional switch end - complete module run
#

View File

@@ -1,6 +1,6 @@
#
# Digital Enegry Cloud Orchestration Technology (DECORT) modules for Ansible
# Copyright: (c) 2018-2020 Digital Energy Cloud Solutions LLC
# Copyright: (c) 2018-2021 Digital Energy Cloud Solutions LLC
#
# Apache License 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.txt)
#
@@ -28,7 +28,7 @@ Requirements:
- PyJWT Python module
- requests Python module
- netaddr Python module
- DECORT cloud platform version 3.4.2 or higher
- DECORT cloud platform version 3.6.1 or higher
"""
import copy
@@ -40,6 +40,7 @@ import requests
from ansible.module_utils.basic import AnsibleModule
#
# TODO: the following functionality to be implemented and/or tested
# 4) workflow callbacks
@@ -150,7 +151,7 @@ class DecortController(object):
if self.authenticator == "jwt":
# validate supplied JWT on the DECORT controller
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']
elif self.authenticator == "legacy":
# 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
self.obtain_oauth2_jwt()
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.run_phase = "Initializing DecortController instance complete."
@@ -272,7 +273,7 @@ class DecortController(object):
# never be executed
return False
req_url = self.controller_url + "/restmachine/cloudapi/accounts/list"
req_url = self.controller_url + "/restmachine/cloudapi/account/list"
req_header = dict(Authorization="bearer {}".format(arg_jwt), )
try:
@@ -316,7 +317,7 @@ class DecortController(object):
self.amodule.fail_json(**self.result)
return False
req_url = self.controller_url + "/restmachine/cloudapi/users/authenticate"
req_url = self.controller_url + "/restmachine/cloudapi/user/authenticate"
req_data = dict(username=self.user,
password=self.password, )
@@ -388,7 +389,8 @@ class DecortController(object):
return None # actually, this directive will never be executed as fail_json aborts the script
except requests.exceptions.Timeout:
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)
return None
@@ -518,8 +520,11 @@ class DecortController(object):
#
# then all values when entering this method will be of type string. We need to
# explicitly cast int type on all of them.
if new_data_disks is not None:
for idx, repair in enumerate(new_data_disks):
new_data_disks[idx] = int(repair)
else:
new_data_disks = []
for disk in comp_dict['disks']:
if disk['type'] == 'B':
@@ -602,7 +607,6 @@ class DecortController(object):
return ret_comp_id, ret_comp_dict, ret_rg_id
def compute_find(self, comp_id,
comp_name="", rg_id=0,
check_state=True):
@@ -649,7 +653,8 @@ class DecortController(object):
# Therefore, RG ID cannot be zero and compute name cannot be empty.
if not rg_id and comp_name == "":
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)
# fail the module - exit
@@ -766,8 +771,8 @@ class DecortController(object):
@param (int) rg_id: ID of the RG where the VM will be provisioned.
@param (string) comp_name: that specifies the name of the VM.
@param (string) arch: hardware architecture of KVM VM. Supported values are: "KVM_X86" for Intel x86
and "KVM_PPC" for IBM PowerPC.
@param (string) arch: hardware architecture of KVM VM. Supported values are: "X86_64" for Intel x86
and "PPC64_LE" for IBM PowerPC.
@param (int) cpu: how many virtual CPUs to allocate.
@param (int) ram: volume of RAM in MB to allocate (i.e. pass 4096 to allocate 4GB RAM).
@param (int) boot_disk: boot disk size in GB.
@@ -789,9 +794,9 @@ class DecortController(object):
return 0
api_url = ""
if arch == "KVM_X86":
if arch == "X86_64":
api_url = "/restmachine/cloudapi/kvmx86/create"
elif arch == "KVM_PPC":
elif arch == "PPC64_LE":
api_url = "/restmachine/cloudapi/kvmppc/create"
else:
self.result['failed'] = True
@@ -864,19 +869,29 @@ class DecortController(object):
api_params = dict(accountId=comp_dict['accountId'])
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/search", api_params)
vins_list = json.loads(api_resp.content.decode('utf8'))
if not len(vins_list):
self.result['failed'] = True
self.result['msg'] = ("compute_networks() cannot obtain VINS list for Account ID {}, "
"Compute ID {}.").format(comp_dict['accountId'], comp_dict['id'])
return
#
# We should not fail the module if ViNS list is empty - it is not an error, as in case of
# API failure "decort_api_call" will abort the module execution on its own. Hence the
# following code fragment is commented out
#
# if not len(vins_list):
# self.result['failed'] = True
# self.result['msg'] = ("compute_networks() cannot obtain VINS list for Account ID {}, "
# "Compute ID {}.").format(comp_dict['accountId'], comp_dict['id'])
# return
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/externalnetwork/list", api_params)
extnet_list = json.loads(api_resp.content.decode('utf8')) # list of dicts: "name" holds "NET_ADDR/NETMASK", "id" is ID
if not len(vins_list):
self.result['failed'] = True
self.result['msg'] = ("compute_networks() cannot obtain External networks list for Account ID {}, "
"Compute ID {}.").format(comp_dict['accountId'], comp_dict['id'])
return
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
#
# 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.
#
# if not len(extnet_list):
# self.result['failed'] = True
# self.result['msg'] = ("compute_networks() cannot obtain External networks list for Account ID {}, "
# "Compute ID {}.").format(comp_dict['accountId'], comp_dict['id'])
# return
# 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)
@@ -902,6 +917,19 @@ class DecortController(object):
mac=iface['mac'])
enet_iface_list.append(iface_data)
# If at this point compt_dict["interfaces"] lists some interfaces, but neither vins_iface_list
# nor enet_iface_list contain any members, it means that none of the ViNS or Ext Nets currently
# available to us match existing interfaces of the Compute instance.
# This is abnormal condition and we should not proceed any further.
if len(comp_dict['interfaces']) and (not len(vins_iface_list) and not len(enet_iface_list)):
self.result['failed'] = True
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']),
comp_dict['id'],
len(vins_list),
len(extnet_list))
return
vins_id_list = [rec['id'] for rec in vins_iface_list]
enet_id_list = [rec['id'] for rec in enet_iface_list]
@@ -1165,17 +1193,16 @@ class DecortController(object):
# OS image manipulation methods
###################################
def _image_get_by_id(self, image_id):
# TODO: update once cloudapi/images/get is implemented, see ticket #2963
# TODO: update once cloudapi/image/get is implemented, see ticket #2963
api_params = dict(imageId=image_id,
showAll=False)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/images/get", api_params)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/image/get", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json.
ret_image_dict = json.loads(api_resp.content.decode('utf8'))
return image_id, ret_image_dict
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.
Primary use of this function is to obtain the ID of the image identified by its name and,
@@ -1204,7 +1231,7 @@ class DecortController(object):
if image_id > 0:
ret_image_id, ret_image_dict = self._image_get_by_id(image_id)
if (ret_image_id and
(sepid == 0 or sepid == ret_image_dict['sepid']) and
(sepid == 0 or sepid == ret_image_dict['sepId']) and
(pool == "" or pool == ret_image_dict['pool'])):
return ret_image_id, ret_image_dict
else:
@@ -1218,7 +1245,7 @@ class DecortController(object):
validated_acc_id = rg_facts['accountId']
api_params = dict(accountId=validated_acc_id)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/images/list", api_params)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/image/list", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json.
images_list = json.loads(api_resp.content.decode('utf8'))
for image_record in images_list:
@@ -1228,7 +1255,7 @@ class DecortController(object):
return image_record['id'], image_record
# if positive SEP ID and/or non-emtpy pool name are passed, match by them
full_match = True
if sepid > 0 and sepid != image_record['sepid']:
if sepid > 0 and sepid != image_record['sepId']:
full_match = False
if pool != "" and pool != image_record['pool']:
full_match = False
@@ -1355,12 +1382,12 @@ class DecortController(object):
self.amodule.fail_json(**self.result)
# 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)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/account/listRG", api_params)
if api_resp.status_code == 200:
account_specs = json.loads(api_resp.content.decode('utf8'))
api_params.pop('accountId')
for rg_id in account_specs['rgs']:
got_id, got_specs = self._rg_get_by_id(rg_id)
for rg_item in account_specs:
got_id, got_specs = self._rg_get_by_id(rg_item['id'])
if got_id and got_specs['name'] == arg_rg_name:
# name matches
if not arg_check_state or got_specs['status'] not in RG_INVALID_STATES:
@@ -1377,7 +1404,6 @@ class DecortController(object):
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=""):
"""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
@@ -1409,7 +1435,8 @@ class DecortController(object):
target_gid = self.gid_get(arg_location)
if not target_gid:
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)
api_params = dict(accountId=arg_account_id,
@@ -1535,7 +1562,8 @@ class DecortController(object):
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"]
if arg_rg_dict['status'] in NOP_STATES_FOR_RG_CHANGE:
@@ -1608,12 +1636,12 @@ class DecortController(object):
if account_name == "":
api_params['accountId'] = account_id
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/accounts/get", api_params)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/account/get", api_params)
if api_resp.status_code == 200:
account_details = json.loads(api_resp.content.decode('utf8'))
return account_details['id'], account_details
else:
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/accounts/list", api_params)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/account/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
@@ -1623,7 +1651,7 @@ class DecortController(object):
# 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)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/account/get", api_params)
if api_resp.status_code == 200:
account_details = json.loads(api_resp.content.decode('utf8'))
return account_details['id'], account_details
@@ -2047,7 +2075,8 @@ class DecortController(object):
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"]
if vins_dict['status'] in NOP_STATES_FOR_VINS_CHANGE:
@@ -2122,7 +2151,7 @@ class DecortController(object):
"was requested.").format(vins_dict['id'], vins_dict['name'])
return
if not vins_dict['rgid']:
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'])
@@ -2170,7 +2199,8 @@ class DecortController(object):
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'])
gw_config[
'ext_net_id'])
return
@@ -2260,7 +2290,8 @@ class DecortController(object):
if self.amodule.check_mode:
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
ret_disk_id = 0
@@ -2301,7 +2332,7 @@ class DecortController(object):
return 0, None
def disk_provision(self, disk_name, size, account_id, sep_id, pool="default", desc="", location=""):
def disk_provision(self, disk_name, size, account_id, sep_id, pool_name="", desc="", location=""):
"""Provision Disk according to the specified arguments.
Note that disks created by this method will be of type 'D' (data disks).
If critical error occurs the embedded call to API function will abort further execution
@@ -2323,7 +2354,8 @@ class DecortController(object):
if self.amodule.check_mode:
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
target_gid = self.gid_get(location)
@@ -2336,11 +2368,12 @@ class DecortController(object):
api_params = dict(accountId=account_id,
gid=target_gid,
name=disk_name,
description=desc,
desc=desc,
size=size,
type='D',
sep_id=sep_id,
pool=pool,)
sepId=sep_id, )
if pool_name != "":
api_params['pool'] = pool_name
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/create", api_params)
if api_resp.status_code == 200:
ret_disk_id = json.loads(api_resp.content.decode('utf8'))
@@ -2374,7 +2407,8 @@ class DecortController(object):
if not new_size:
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
if new_size < disk_facts['sizeMax']:
@@ -2425,7 +2459,6 @@ class DecortController(object):
self.result['changed'] = True
return
##############################
#
# Port Forward rules management
@@ -2500,7 +2533,8 @@ class DecortController(object):
break
else:
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
existing_rules = []
@@ -2615,3 +2649,256 @@ class DecortController(object):
ret_rules = self._pfw_get(comp_facts['id'], vins_facts['id'])
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