Compare commits

...

2 Commits
main ... rc-5.0

@ -5,8 +5,8 @@ Note that this module may produce unreliable results when used with older DECORT
Requirements:
* Ansible 2.7 or higher
* Python 2.6 or higher
* PyJWT 1.7.1 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.5.0 or higher

@ -0,0 +1,36 @@
---
#
# DECORT kvmvm module example
#
- hosts: ansible_master
tasks:
- name: create a VM named cloud-init_example
decort_kvmvm:
name: affinity_example
annotation: "VM managed by decort_kvmvm module"
authenticator: oauth2
app_id: "" # Application id from SSO Digital Energy
app_secret: "" # API key from SSO Digital Energy
controller_url: "" #"https://mr4.digitalenergy.online"
rg_id: # Resource group id
cpu: 2
ram: 2048
boot_disk: 10
image_name: "DECS Ubuntu 18.04 v1.2.3" # Name of OS image
networks:
- type: VINS
id: # VINS id
tags: "Ansible cloud init example"
aff_lable: "Affinity lable"
tag:
- key: bd
value: main
aff_rule:
- key: app
value: main
topology: compute
policy: REQUIRED
mode: EQ
state: present
delegate_to: localhost
register: simple_vm

@ -0,0 +1,36 @@
---
#
# DECORT kvmvm module example
#
- hosts: ansible_master
tasks:
- name: create a VM named cloud-init_example
decort_kvmvm:
name: anti-affinity_example
annotation: "VM managed by decort_kvmvm module"
authenticator: oauth2
app_id: "" # Application id from SSO Digital Energy
app_secret: "" # API key from SSO Digital Energy
controller_url: "" #"https://mr4.digitalenergy.online"
rg_id: # Resource group id
cpu: 2
ram: 2048
boot_disk: 10
image_name: "DECS Ubuntu 18.04 v1.2.3" #Name of OS image
networks:
- type: VINS
id: #VINS id
tags: "Ansible cloud init example"
aff_lable: "Anti affinity lable"
tag:
- key: bd
value: main
aaff_rule:
- key: app
value: main
topology: compute
policy: REQUIRED
mode: ANY
state: present
delegate_to: localhost
register: simple_vm

@ -0,0 +1,38 @@
#
# DECORT kvmvm module example
#
- hosts: ansible_master
tasks:
- name: create a VM named cloud-init_example
decort_kvmvm:
annotation: "VM managed by decort_kvmvm module"
authenticator: oauth2
app_id: "" # Application id from SSO Digital Energy
app_secret: "" # API key from SSO Digital Energy
controller_url: "" #"https://mr4.digitalenergy.online"
name: cloud-init_example
cpu: 2
ram: 2048
boot_disk: 10
image_name: "DECS Ubuntu 18.04 v1.2.3" #Name of OS image
networks:
- type: VINS
id: #VINS id
tags: "Ansible cloud init example"
state: present
rg_id: #Resource group id
ci_user_data:
- packages:
- apache2
- write_files:
- content: |
<div>
Hello World!
</div>
owner: user:user
path: /var/www/html/index.html
- hostname: test-apache
- ssh_keys:
- rsa_public: ssh-rsa AAAAOasDmLxnD= user@pc
delegate_to: localhost
register: simple_vm

@ -0,0 +1,39 @@
---
#
# DECORT k8s module example
#
- hosts: ansible_master
tasks:
- name: obtain JWT
decort_jwt:
oauth2_url: "" #"https://sso.digitalenergy.online"
validity: 1200
verify_ssl: false
register: token
delegate_to: localhost
- name: create a VM named cloud-init_example
decort_k8s:
state: present
started: True
getConfig: True
authenticator: jwt
jwt: "{{ token.jwt }}"
controller_url: "" #"https://mr4.digitalenergy.online"
name: "cluster-test"
rg_id: # Resource group id
k8ci_id: # k8s ci id
workers:
- name: wg1
ram: 1024
cpu: 10
disk: 10
num: 1
- name: wg2
ram: 1024
cpu: 10
disk: 10
num: 2
verify_ssl: false
delegate_to: localhost
register: kube

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

@ -0,0 +1,340 @@
#!/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 *
class decort_k8s(DecortController):
def __init__(self,arg_amodule):
super(decort_k8s, self).__init__(arg_amodule)
validated_acc_id = 0
validated_rg_id = 0
validated_rg_facts = None
validated_k8ci_id = 0
if arg_amodule.params['name'] == "" and arg_amodule.params['id'] == 0:
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] = "Cannot manage k8s cluster when its ID is 0 and name is empty."
self.fail_json(**self.result)
if not arg_amodule.params['id']:
if not arg_amodule.params['rg_id']: # RG ID is not set -> locate RG by name -> need account ID
validated_acc_id, _ = self.account_find(arg_amodule.params['account_name'],
arg_amodule.params['account_id'])
if not validated_acc_id:
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] = ("Current user does not have access to the account ID {} / "
"name '{}' or non-existent account specified.").format(arg_amodule.params['account_id'],
arg_amodule.params['account_name'])
self.fail_json(**self.result)
# fail the module -> exit
# now validate RG
validated_rg_id, validated_rg_facts = self.rg_find(validated_acc_id,
arg_amodule.params['rg_id'],)
if not validated_rg_id:
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] = "Cannot find RG ID {} / name '{}'.".format(arg_amodule.params['rg_id'],
arg_amodule.params['rg_name'])
self.fail_json(**self.result)
# fail the module - exit
#validate k8ci ID
validated_k8ci_id = self.k8s_k8ci_find(arg_amodule.params['k8ci_id'])
if not validated_k8ci_id:
self.result['failed'] = True
self.result['changed'] = False
self.result['msg'] = "Cannot find K8CI ID {}.".format(arg_amodule.params['k8ci_id'])
self.fail_json(**self.result)
self.rg_id = validated_rg_id
arg_amodule.params['rg_id'] = validated_rg_id
arg_amodule.params['rg_name'] = validated_rg_facts['name']
self.acc_id = validated_rg_facts['accountId']
arg_amodule.params['k8ci_id'] = validated_k8ci_id
self.k8s_id,self.k8s_info = self.k8s_find(k8s_id=arg_amodule.params['id'],
k8s_name=arg_amodule.params['name'],
rg_id=validated_rg_id,
check_state=False)
if self.k8s_id:
self.k8s_should_exist = True
self.acc_id = self.k8s_info['accountId']
# check workers and groups for add or remove
return
def package_facts(self,check_mode=False):
ret_dict = dict(
name="",
state="CHECK_MODE",
account_id=0,
rg_id=0,
config=None,
)
if check_mode:
# in check mode return immediately with the default values
return ret_dict
#if self.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'] = self.k8s_info['id']
ret_dict['name'] = self.k8s_info['name']
ret_dict['techStatus'] = self.k8s_info['techStatus']
ret_dict['state'] = self.k8s_info['status']
ret_dict['rg_id'] = self.rg_id
ret_dict['account_id'] = self.acc_id
if self.amodule.params['getConfig'] and self.k8s_info['techStatus'] == "STARTED":
ret_dict['config'] = self.k8s_getConfig()
return ret_dict
def nop(self):
"""No operation (NOP) handler for Compute management by decort_kvmvm module.
This function is intended to be called from the main switch construct of the module
when current state -> desired state change logic does not require any changes to
the actual Compute state.
"""
self.result['failed'] = False
self.result['changed'] = False
if self.k8s_id:
self.result['msg'] = ("No state change required for K8s ID {} because of its "
"current status '{}'.").format(self.k8s_id, self.k8s_info['status'])
else:
self.result['msg'] = ("No state change to '{}' can be done for "
"non-existent K8s instance.").format(self.amodule.params['state'])
return
def error(self):
self.result['failed'] = True
self.result['changed'] = False
if self.k8s_id:
self.result['msg'] = ("Invalid target state '{}' requested for K8s cluster ID {} in the "
"current status '{}'.").format(self.k8s_id,
self.amodule.params['state'],
self.k8s_info['status'])
else:
self.result['msg'] = ("Invalid target state '{}' requested for non-existent K8s Cluster name '{}' "
"in RG ID {} / name '{}'").format(self.amodule.params['state'],
self.amodule.params['name'],
self.amodule.params['rg_id'],
self.amodule.params['rg_name'])
return
def create(self):
self.k8s_provision(self.amodule.params['name'],
self.amodule.params['workers'][0]['name'],
self.amodule.params['k8ci_id'],
self.amodule.params['rg_id'],
self.amodule.params['master_count'],
self.amodule.params['master_cpu'],
self.amodule.params['master_ram_mb'],
self.amodule.params['master_disk_gb'],
self.amodule.params['workers'][0]['num'],
self.amodule.params['workers'][0]['cpu'],
self.amodule.params['workers'][0]['ram'],
self.amodule.params['workers'][0]['disk'],
self.amodule.params['extnet_id'],
self.amodule.params['with_lb'],
self.amodule.params['description'],)
self.k8s_id,self.k8s_info = self.k8s_find(k8s_id=self.amodule.params['id'],
k8s_name=self.amodule.params['name'],
rg_id=self.rg_id,
check_state=False)
if self.k8s_id:
self.k8s_should_exist = True
if self.k8s_id and self.amodule.params['workers'][1]:
self.k8s_workers_modify(self.k8s_info,self.amodule.params['workers'])
return
def destroy(self):
self.k8s_delete(self.k8s_id)
self.k8s_info['status'] = 'DELETED'
self.k8s_should_exist = False
return
def action(self,disared_state,started=True):
self.k8s_state(self.k8s_info, disared_state,started)
self.k8s_id,self.k8s_info = self.k8s_find(k8s_id=self.amodule.params['id'],
k8s_name=self.amodule.params['name'],
rg_id=self.rg_id,
check_state=False)
if started == True and self.k8s_info['techStatus'] == "STOPPED":
self.k8s_state(self.k8s_info, disared_state,started)
self.k8s_info['techStatus'] == "STARTED"
self.k8s_workers_modify(self.k8s_info,self.amodule.params['workers'])
return
@staticmethod
def build_parameters():
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','check']),
permanent=dict(type='bool', default=False),
started=dict(type='bool', default=True),
user=dict(type='str',
required=False,
fallback=(env_fallback, ['DECORT_USER'])),
name=dict(type='str', required=True),
id=dict(type='int', required=False, default=0),
getConfig=dict(type='bool',required=False, default=False),
rg_id=dict(type='int', default=0),
rg_name=dict(type='str',default=""),
k8ci_id=dict(type='int', required=True),
wg_name=dict(type='str', required=False),
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=10),
workers=dict(type='list'),
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.build_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'],
],
required_one_of=[
['id', 'name'],
['rg_id','rg_name']
],
)
subj = decort_k8s(amodule)
if amodule.params['state'] == 'check':
subj.result['changed'] = False
if subj.k8s_id:
# cluster is found - package facts and report success to Ansible
subj.result['failed'] = False
subj.result['facts'] = subj.package_facts(amodule.check_mode)
amodule.exit_json(**subj.result)
# we exit the module at this point
else:
subj.result['failed'] = True
subj.result['msg'] = ("Cannot locate K8s cluster name '{}'. "
"RG ID {}").format(amodule.params['name'],
amodule.params['rg_id'],)
amodule.fail_json(**subj.result)
if subj.k8s_id:
if subj.k8s_info['status'] in ("DELETING","DESTROYNG","CREATING","DESTROYING",
"ENABLING","DISABLING","RESTORING","MODELED"):
subj.error()
elif subj.k8s_info['status'] == "DELETED":
if amodule.params['state'] in ('disabled', 'enabled', 'present'):
subj.k8s_restore(subj.k8s_id)
subj.action(amodule.params['state'])
if amodule.params['state'] == 'absent':
subj.nop()
elif subj.k8s_info['techStatus'] in ("STARTED","STOPPED"):
if amodule.params['state'] == 'disabled':
subj.action(amodule.params['state'])
elif amodule.params['state'] == 'absent':
subj.destroy()
else:
subj.action(amodule.params['state'],amodule.params['started'])
elif subj.k8s_info['status'] == "DISABLED":
if amodule.params['state'] == 'absent':
subj.destroy()
elif amodule.params['state'] in ('present','enabled'):
subj.action(amodule.params['state'],amodule.params['started'])
else:
subj.nop()
elif subj.k8s_info['status'] == "DESTROED":
if amodule.params['state'] in ('present','enabled'):
subj.create()
if amodule.params['state'] == 'absent':
subj.nop()
else:
if amodule.params['state'] == 'absent':
subj.nop()
if amodule.params['state'] in ('present','started'):
subj.create()
elif amodule.params['state'] in ('stopped', 'disabled','enabled'):
subj.error()
if subj.result['failed']:
amodule.fail_json(**subj.result)
else:
if subj.k8s_should_exist:
subj.result['facts'] = subj.package_facts(amodule.check_mode)
amodule.exit_json(**subj.result)
else:
amodule.exit_json(**subj.result)
if __name__ == "__main__":
main()

@ -192,6 +192,9 @@ options:
- If I(ssh_key) is not specified, this parameter is ignored and a warning is generated.
- This parameter is valid at VM creation time only and ignored for any operation on existing VMs.
required: no
user_data:
description:
- Cloud-init User-Data, exept ssh module
state:
description:
- Specify the desired state of the virtual machine at the exit of the module.
@ -548,15 +551,18 @@ class decort_kvmvm(DecortController):
if self.amodule.params['state'] in ('halted', 'poweredoff'):
start_compute = False
if self.amodule.params['ssh_key'] and self.amodule.params['ssh_key_user']:
if self.amodule.params['ssh_key'] and self.amodule.params['ssh_key_user'] and not self.amodule.params['ci_user_data']:
cloud_init_params = {'users': [
{"name": self.amodule.params['ssh_key_user'],
"ssh-authorized-keys": [self.amodule.params['ssh_key']],
"shell": '/bin/bash'}
]}
elif self.amodule.params['ci_user_data']:
cloud_init_params = {}
for ci_param in self.amodule.params['ci_user_data']:
cloud_init_params.update(ci_param)
else:
cloud_init_params = None
# if we get through here, all parameters required to create new Compute instance should be at hand
# NOTE: KVM VM is created in HALTED state and must be explicitly started
@ -595,6 +601,11 @@ class decort_kvmvm(DecortController):
# Next manage data disks
self.compute_data_disks(self.comp_info, self.amodule.params['data_disks'])
self.compute_affinity(self.comp_info,
self.amodule.params['tag'],
self.amodule.params['aff_rule'],
self.amodule.params['aaff_rule'],
label=self.amodule.params['affinity_label'],)
# NOTE: see NOTE above regarding libvirt "feature" and new VMs created in HALTED state
if self.amodule.params['state'] not in ('halted', 'poweredoff'):
self.compute_powerstate(self.comp_info, 'started')
@ -641,6 +652,11 @@ class decort_kvmvm(DecortController):
self.compute_resize(self.comp_info,
self.amodule.params['cpu'], self.amodule.params['ram'],
wait_for_state_change=arg_wait_cycles)
self.compute_affinity(self.comp_info,
self.amodule.params['tag'],
self.amodule.params['aff_rule'],
self.amodule.params['aaff_rule'],
label=self.amodule.params['affinity_label'],)
return
def package_facts(self, check_mode=False):
@ -774,6 +790,11 @@ class decort_kvmvm(DecortController):
rg_name=dict(type='str', default=""),
ssh_key=dict(type='str', required=False),
ssh_key_user=dict(type='str', required=False),
tag=dict(type='list', required=False),
affinity_label=dict(type='str', required=False),
aff_rule=dict(type='list', required=False),
aaff_rule=dict(type='list', required=False),
ci_user_data=dict(type='list', required=False),
state=dict(type='str',
default='present',
choices=['absent', 'paused', 'poweredoff', 'halted', 'poweredon', 'present', 'check']),

@ -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
@ -383,12 +384,12 @@ def main():
decon.result['failed'] = True
decon.result['msg'] = "Specified ViNS ID {} not found.".format(amodule.params['vins_id'])
decon.fail_json(**decon.result)
vins_level="ID"
vins_level = "ID"
validated_acc_id = vins_facts['accountId']
validated_rg_id = vins_facts['rgId']
elif amodule.params['rg_id']:
# expect ViNS @ RG level in the RG with specified ID
vins_level="RG"
vins_level = "RG"
# This call to rg_find will abort the module if no RG with such ID is present
validated_rg_id, rg_facts = decon.rg_find(0, # account ID set to 0 as we search for RG by RG ID
amodule.params['rg_id'], arg_rg_name="")
@ -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
#

@ -24,7 +24,7 @@ NOTE: this utility library requires DECORT platform version 3.4.0 or higher.
It is not compatible with older versions.
Requirements:
- python >= 2.6
- python >= 3.8
- PyJWT Python module
- requests Python module
- netaddr Python module
@ -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."
@ -205,7 +206,7 @@ class DecortController(object):
client_id=self.app_id,
client_secret=self.app_secret,
response_type="id_token",
validity=3600,)
validity=3600, )
# TODO: Need standard code snippet to handle server timeouts gracefully
# Consider a few retries before giving up or use requests.Session & requests.HTTPAdapter
# see https://stackoverflow.com/questions/15431044/can-i-set-max-retries-for-requests-request
@ -273,7 +274,7 @@ class DecortController(object):
return False
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:
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_data = dict(username=self.user,
password=self.password,)
password=self.password, )
try:
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
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
@ -536,16 +538,16 @@ class DecortController(object):
if disk['id'] not in new_data_disks:
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:
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)
# On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['changed'] = True
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)
# On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['changed'] = True
@ -572,7 +574,7 @@ class DecortController(object):
return
api_params = dict(computeId=comp_id,
permanently=permanently,)
permanently=permanently, )
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/delete", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False
@ -593,7 +595,7 @@ class DecortController(object):
ret_comp_dict = None
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)
if api_resp.status_code == 200:
ret_comp_id = comp_id
@ -605,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):
@ -652,11 +653,12 @@ 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
api_params = dict(includedeleted=True,)
api_params = dict(includedeleted=True, )
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/list", api_params)
if api_resp.status_code == 200:
comp_list = json.loads(api_resp.content.decode('utf8'))
@ -791,7 +793,7 @@ class DecortController(object):
"was requested.").format(comp_name, rg_id)
return 0
api_url=""
api_url = ""
if arch == "X86_64":
api_url = "/restmachine/cloudapi/kvmx86/create"
elif arch == "PPC64_LE":
@ -864,7 +866,7 @@ class DecortController(object):
for repair in new_networks:
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)
vins_list = json.loads(api_resp.content.decode('utf8'))
#
@ -879,7 +881,8 @@ class DecortController(object):
# 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
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.
@ -927,20 +930,20 @@ class DecortController(object):
len(extnet_list))
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
attach_list = [] # attach list holds both ViNS and Ext Net attachment specs, as API handles them the same way
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'],
netType='VINS',
netId=netrunner['id'],
ipAddr=netrunner.get('ip_addr', ""))
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'],
netType='EXTNET',
netId=netrunner['id'],
@ -952,7 +955,7 @@ class DecortController(object):
# 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
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:
if netrunner['id'] not in target_list:
@ -961,7 +964,7 @@ class DecortController(object):
mac=netrunner['mac'])
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:
if netrunner['id'] not in target_list:
@ -1093,7 +1096,7 @@ class DecortController(object):
if not new_cpu and not new_ram:
# if both are 0 or Null - return immediately, as user did not mean to manage size
self.result['failed'] = False
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'])
return
@ -1104,12 +1107,12 @@ class DecortController(object):
# stupid hack?
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:
# no need to call API in this case, as requested size is not different from the current one
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'])
return
@ -1125,7 +1128,7 @@ class DecortController(object):
if not wait_for_state_change:
self.result['failed'] = True
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'],
comp_dict['cpus'], comp_dict['ram'],
new_cpu, new_ram, comp_dict['status'])
@ -1133,7 +1136,7 @@ class DecortController(object):
api_params = dict(computeId=comp_dict['id'],
ram=new_ram,
cpu=new_cpu,)
cpu=new_cpu, )
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/resize", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False
@ -1186,6 +1189,47 @@ class DecortController(object):
return False
def compute_affinity(self,comp_dict,tags,aff,aaff,label=""):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "compute_affinity")
api_params = dict(computeId=comp_dict['id'])
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/affinityRulesClear", api_params)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/antiAffinityRulesClear", api_params)
if tags:
for tag in tags:
api_params = dict(computeId=comp_dict['id'],
key=tag['key'],
value=tag['value'], )
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/tagAdd", api_params)
if label:
api_params = dict(computeId=comp_dict['id'],
affinityLabel=label,)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/affinityLabelSet", api_params)
if aff:
if len(aff[0])>0:
for rule in aff:
api_params = dict(computeId=comp_dict['id'],
key=rule['key'],
value=rule['value'],
topology=rule['topology'],
mode=rule['mode'],
policy=rule['policy'],)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/affinityRuleAdd", api_params)
if aaff:
if len(aaff[0])>0:
for rule in aaff:
api_params = dict(computeId=comp_dict['id'],
key=rule['key'],
value=rule['value'],
topology=rule['topology'],
mode=rule['mode'],
policy=rule['policy'],)
self.decort_api_call(requests.post, "/restmachine/cloudapi/compute/antiAffinityRuleAdd", api_params)
self.result['failed'] = False
self.result['changed'] = True
###################################
# OS image manipulation methods
###################################
@ -1200,7 +1244,6 @@ class DecortController(object):
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,
@ -1228,9 +1271,9 @@ 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
if (ret_image_id 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
else:
validated_acc_id = account_id
@ -1292,7 +1335,7 @@ class DecortController(object):
api_params = dict(rgId=rg_id,
# force=True | False,
permanently=permanently,)
permanently=permanently, )
self.decort_api_call(requests.post, "/restmachine/cloudapi/rg/delete", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False
@ -1317,7 +1360,7 @@ class DecortController(object):
self.result['msg'] = "rg_get_by_id(): zero RG ID specified."
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)
if api_resp.status_code == 200:
ret_rg_id = rg_id
@ -1402,7 +1445,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
@ -1434,7 +1476,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,
@ -1495,12 +1538,12 @@ class DecortController(object):
query_key_map = dict(cpu='CU_C',
ram='CU_M',
disk='CU_D',
ext_ips='CU_I',)
ext_ips='CU_I', )
set_key_map = dict(cpu='maxCPUCapacity',
ram='maxMemoryCapacity',
disk='maxVDiskCapacity',
ext_ips='maxNumPublicIP',)
api_params = dict(rgId=arg_rg_dict['id'],)
ext_ips='maxNumPublicIP', )
api_params = dict(rgId=arg_rg_dict['id'], )
quota_change_required = False
for new_limit in ('cpu', 'ram', 'disk', 'ext_ips'):
@ -1543,7 +1586,7 @@ class DecortController(object):
return
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)
# On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False
@ -1560,7 +1603,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:
@ -1677,7 +1721,7 @@ class DecortController(object):
"requested.").format(arg_type, arg_mode, arg_vmid)
return 0
api_params=dict(
api_params = dict(
machineId=arg_vmid,
gpu_type=arg_type,
gpu_mode=arg_mode,
@ -1709,7 +1753,7 @@ class DecortController(object):
"requested.").format(arg_vgpuid, arg_vmid)
return True
api_params=dict(
api_params = dict(
machineId=arg_vmid,
vgpuid=arg_vgpuid,
)
@ -1734,7 +1778,7 @@ class DecortController(object):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "gpu_list")
api_params=dict(
api_params = dict(
machineId=arg_vmid,
list_destroyed=arg_list_destroyed,
)
@ -1839,7 +1883,7 @@ class DecortController(object):
api_params = dict(vinsId=vins_id,
# force=True | False,
permanently=permanently,)
permanently=permanently, )
self.decort_api_call(requests.post, "/restmachine/cloudapi/vins/delete", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False
@ -1867,7 +1911,7 @@ class DecortController(object):
self.result['msg'] = "vins_get_by_id(): zero ViNS ID specified."
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)
if api_resp.status_code == 200:
ret_vins_id = vins_id
@ -2055,7 +2099,7 @@ class DecortController(object):
return
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)
# On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False
@ -2072,7 +2116,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:
@ -2091,7 +2136,7 @@ class DecortController(object):
if self.amodule.check_mode:
self.result['failed'] = False
self.result['msg'] = ("vins_state() in check mode: setting state of ViNS ID {}, name '{}' to "
"'{}' was requested.").format(vins_dict['id'],vins_dict['name'],
"'{}' was requested.").format(vins_dict['id'], vins_dict['name'],
desired_state)
return
@ -2137,14 +2182,14 @@ class DecortController(object):
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")
if self.amodule.check_mode:
self.result['failed'] = False
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
if not vins_dict['rgId']:
@ -2195,7 +2240,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
@ -2223,7 +2269,7 @@ class DecortController(object):
api_params = dict(diskId=disk_id,
detach=force_detach,
permanently=permanently,)
permanently=permanently, )
self.decort_api_call(requests.post, "/restmachine/cloudapi/disks/delete", api_params)
# On success the above call will return here. On error it will abort execution by calling fail_json.
self.result['failed'] = False
@ -2251,7 +2297,7 @@ class DecortController(object):
self.result['msg'] = "disk_get_by_id(): zero Disk ID specified."
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)
if api_resp.status_code == 200:
ret_disk_id = disk_id
@ -2285,7 +2331,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
@ -2348,7 +2395,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)
@ -2364,7 +2412,7 @@ class DecortController(object):
desc=desc,
size=size,
type='D',
sepId=sep_id,)
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)
@ -2400,7 +2448,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']:
@ -2444,14 +2493,13 @@ class DecortController(object):
return
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)
# 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
##############################
#
# Port Forward rules management
@ -2526,7 +2574,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 = []
@ -2641,3 +2690,368 @@ 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
##############################
#
# K8s management
#
##############################
def k8s_find(self, k8s_id, k8s_name="",rg_id=0,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","DESTROYED","DESTROYING"]
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_find")
ret_k8s_id = 0
api_params = dict(includedeleted=True)
ret_k8s_dict = None
if k8s_id:
ret_k8s_id, ret_k8s_dict = self._k8s_get_by_id(k8s_id)
if not ret_k8s_id:
self.result['failed'] = True
self.result['msg'] = "k8s_find(): cannot find k8s cluster by ID {}.".format(k8s_id)
self.amodule.fail_json(**self.result)
else:
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/list", api_params)
if api_resp.status_code == 200:
k8s_list = json.loads(api_resp.content.decode('utf8'))
for k8s_item in k8s_list:
if k8s_item['name'] == k8s_name and k8s_item['rgId'] == rg_id:
if not check_state or k8s_item['status'] not in K8S_INVALID_STATES:
ret_k8s_id = k8s_item['id']
_, ret_k8s_dict = self._k8s_get_by_id(ret_k8s_id)
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
if arg_desired_state == 'present':
arg_desired_state = 'enabled'
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 K8s cluster ID {} was requested.".format(k8s_id)
return
api_params = dict(k8sId=k8s_id,
permanently=False,
)
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['msg'] = "k8s_delete() K8s cluster ID {} was deleted.".format(k8s_id)
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_enable(self,k8s_id):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_enable")
api_params = dict(k8sId=k8s_id)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/enable", api_params)
self.result['failed'] = False
self.result['changed'] = True
return
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
def k8s_workers_modify(self,arg_k8swg,arg_modwg):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_workers_modify")
if self.k8s_info['techStatus'] != "STARTED":
self.result['changed'] = False
self.result['msg'] = ("k8s_workers_modify(): Can't modify with TechStatus other then STARTED")
return
wg_del_list = []
wg_add_list = []
wg_modadd_list = []
wg_moddel_list = []
wg_outer = [rec['name'] for rec in arg_modwg]
wg_inner = [rec['name'] for rec in arg_k8swg['k8sGroups']['workers']]
for rec in arg_k8swg['k8sGroups']['workers']:
if rec['name'] not in wg_outer:
wg_del_list.append(rec['id'])
for rec in arg_modwg:
if rec['name'] not in wg_inner:
wg_add_list.append(rec)
for rec_inn in arg_k8swg['k8sGroups']['workers']:
for rec_out in arg_modwg:
if rec_inn['num'] != rec_out['num']:
count = rec_inn['num']-rec_out['num']
cmp_list = []
if count > 0:
for cmp in rec_inn['detailedInfo'][:count]:
cmp_list.append(cmp['id'])
wg_moddel_list.append({rec_inn['id']:cmp_list})
if count < 0:
wg_modadd_list.append({rec_inn['id']:abs(count)})
if wg_del_list:
for wgid in wg_del_list:
api_params = dict(k8sId=self.k8s_id,workersGroupId=wgid)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/workersGroupDelete", api_params)
self.result['changed'] = True
if wg_add_list:
for wg in wg_add_list:
api_params = dict(k8sId=self.k8s_id,
name=wg['name'],
workerNum=wg['num'],
workerCpu=wg['cpu'],
workerRam=wg['ram'],
workerDisk=wg['disk'],
)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/workersGroupAdd", api_params)
self.result['changed'] = True
if wg_modadd_list:
for wg in wg_modadd_list:
for key in wg:
api_params = dict(k8sId=self.k8s_id,workersGroupId=key,num=wg[key])
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/workerAdd", api_params)
self.result['changed'] = True
if wg_moddel_list:
for wg in wg_moddel_list:
for key in wg:
for cmpid in wg[key]:
api_params = dict(k8sId=self.k8s_id,workersGroupId=key,workerId=cmpid)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/deleteWorkerFromGroup", api_params)
self.result['changed'] = True
self.result['failed'] = False
return
def k8s_k8ci_find(self,arg_k8ci_id):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_k8ci_find")
api_params = dict(includeDisabled=False)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8ci/list", api_params)
if api_resp.status_code == 200:
ret_k8ci_list = json.loads(api_resp.content.decode('utf8'))
for k8ci_item in ret_k8ci_list:
if k8ci_item['id'] == arg_k8ci_id:
break
else:
self.result['failed'] = True
self.result['msg'] = "k8s_k8ci_find(): cannot find ID."
self.amodule.fail_json(**self.result)
else:
self.result['failed'] = True
self.result['msg'] = ("Failed to get k8ci list HTTP code {}.").format(api_resp.status_code)
self.amodule.fail_json(**self.result)
return arg_k8ci_id
def k8s_getConfig(self):
self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_getConfig")
api_params = dict(k8sId=self.k8s_id)
api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/getConfig", api_params)
ret_conf = api_resp.content.decode('utf8')
return ret_conf

Loading…
Cancel
Save