diff --git a/examples/affinity.yaml b/examples/affinity.yaml new file mode 100644 index 0000000..724fcb5 --- /dev/null +++ b/examples/affinity.yaml @@ -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 \ No newline at end of file diff --git a/examples/anti_affinity.yaml b/examples/anti_affinity.yaml new file mode 100644 index 0000000..0baa8a2 --- /dev/null +++ b/examples/anti_affinity.yaml @@ -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 \ No newline at end of file diff --git a/examples/cloud-init.yaml b/examples/cloud-init.yaml new file mode 100644 index 0000000..4163c0b --- /dev/null +++ b/examples/cloud-init.yaml @@ -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: | +
+ Hello World! +
+ 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 diff --git a/examples/kubernetes.yaml b/examples/kubernetes.yaml new file mode 100644 index 0000000..8e73050 --- /dev/null +++ b/examples/kubernetes.yaml @@ -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 \ No newline at end of file diff --git a/library/decort_k8s.py b/library/decort_k8s.py index 3e3053b..1ed5d55 100644 --- a/library/decort_k8s.py +++ b/library/decort_k8s.py @@ -14,105 +14,247 @@ 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) -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. + validated_acc_id = 0 + validated_rg_id = 0 + validated_rg_facts = None + validated_k8ci_id = 0 - @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 - """ + 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) - 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 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 + - if arg_k8s_facts is None: - # if void facts provided - change state value to ABSENT and return - ret_dict['state'] = "ABSENT" - return ret_dict + #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 - 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'] + 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 - return ret_dict + 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 decort_k8s_parameters(): - """Build and return a dictionary of parameters expected by decort_k8s module in a form accepted - by AnsibleModule utility class.""" + 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 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', + 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_APP_ID'])), - app_secret=dict(type='str', + 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_APP_SECRET']), + fallback=(env_fallback, ['DECORT_PASSWORD']), 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), - ) - + 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_parameters() + module_parameters = decort_k8s.build_parameters() amodule = AnsibleModule(argument_spec=module_parameters, supports_check_mode=True, @@ -125,112 +267,74 @@ def main(): ['app_id', 'app_secret'], ['user', 'password'], ], + required_one_of=[ + ['id', 'name'], + ['rg_id','rg_name'] + ], ) - 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 + 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': - # 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']) + 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: - 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) + 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 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 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() diff --git a/library/decort_kvmvm.py b/library/decort_kvmvm.py index 3b38b07..d2e9af3 100644 --- a/library/decort_kvmvm.py +++ b/library/decort_kvmvm.py @@ -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']), diff --git a/module_utils/decort_utils.py b/module_utils/decort_utils.py index 380fcdf..e08c7ba 100644 --- a/module_utils/decort_utils.py +++ b/module_utils/decort_utils.py @@ -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 @@ -1188,6 +1188,47 @@ class DecortController(object): return True 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 @@ -2678,7 +2719,12 @@ class DecortController(object): return ret_k8s_id, ret_k8s_dict - def k8s_find(self, arg_k8s_id=0, arg_k8s_name="", arg_check_state=True): + ############################## + # + # 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. @@ -2700,7 +2746,7 @@ class DecortController(object): # Transient state (ending with ING) are invalid from k8s manipulation viewpoint # - K8S_INVALID_STATES = ["MODELED"] + K8S_INVALID_STATES = ["MODELED","DESTROYED","DESTROYING"] self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_find") @@ -2708,31 +2754,22 @@ class DecortController(object): 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 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 by ID {}.".format(arg_k8s_id) + self.result['msg'] = "k8s_find(): cannot find k8s cluster by ID {}.".format(k8s_id) self.amodule.fail_json(**self.result) - elif arg_k8s_name != "": + else: 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) + 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 @@ -2753,7 +2790,7 @@ class DecortController(object): "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 {} " @@ -2772,7 +2809,8 @@ class DecortController(object): "'{}' 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 = "" @@ -2802,23 +2840,27 @@ class DecortController(object): "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) + 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=permanently, + 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. @@ -2838,6 +2880,16 @@ class DecortController(object): 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, @@ -2902,3 +2954,104 @@ class DecortController(object): 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 +