From ba8165bcf915737cd58fd5bfa6ffd3dfd0ba846b Mon Sep 17 00:00:00 2001 From: Aleksandr Malyavin Date: Wed, 29 Jun 2022 18:37:56 +0300 Subject: [PATCH] k8s: add labels, taints, annotations. VNFDev: add set IP, enable ssh access, some VNFDev management. add BasicService management. Add examples --- examples/VINS.yaml | 40 ++ examples/annotations.yaml | 40 ++ examples/basicservices.yaml | 31 ++ examples/{cloud-init.yaml => cloud_init.yaml} | 0 library/decort_bservice.py | 289 +++++++++++ library/decort_group.py | 285 +++++++++++ library/decort_k8s.py | 8 +- library/decort_vins.py | 16 +- module_utils/decort_utils.py | 459 +++++++++++++++++- 9 files changed, 1145 insertions(+), 23 deletions(-) create mode 100644 examples/VINS.yaml create mode 100644 examples/annotations.yaml create mode 100644 examples/basicservices.yaml rename examples/{cloud-init.yaml => cloud_init.yaml} (100%) create mode 100644 library/decort_bservice.py create mode 100644 library/decort_group.py diff --git a/examples/VINS.yaml b/examples/VINS.yaml new file mode 100644 index 0000000..929bf96 --- /dev/null +++ b/examples/VINS.yaml @@ -0,0 +1,40 @@ +--- +# +# DECORT vins module example +# + +- hosts: localhost + tasks: + - name: obtain JWT + decort_jwt: + oauth2_url: "https://sso.digitalenergy.online" + validity: 1200 + register: my_jwt + delegate_to: localhost + + - name: print out JWT + debug: + var: my_jwt.jwt + delegate_to: localhost + + - name: Manage ViNS at resource group level + decort_vins: + authenticator: jwt + jwt: "{{ my_jwt.jwt }}" + controller_url: "https://ds1.digitalenergy.online" + vins_name: "vins_created_by_decort_VINS_module" + state: present + rg_id: 198 + ext_net_id: -1 + ipcidr: "10.20.30.0/24" + mgmtaddr: "10.20.30.1" + custom_config: false + config_save: false + verify_ssl: false + + register: managed_vins + + - name: print VINS facter + debug: + msg: "{{managed_vins.facts.password}}" + when: managed_vins.facts.password is defined diff --git a/examples/annotations.yaml b/examples/annotations.yaml new file mode 100644 index 0000000..6177f74 --- /dev/null +++ b/examples/annotations.yaml @@ -0,0 +1,40 @@ +--- +# +# DECORT k8s module labels, taints, annotations example +# + +- hosts: localhost + tasks: + - name: obtain JWT + decort_jwt: + oauth2_url: "https://sso.digitalenergy.online" + validity: 1200 + register: my_jwt + delegate_to: localhost + + - name: print out JWT + debug: + var: my_jwt.jwt + delegate_to: localhost + + - name: Create k8s cluster + decort_k8s: + authenticator: jwt + jwt: "{{ my_jwt.jwt }}" + controller_url: "https://mr4.digitalenergy.online" + name: "example_kubernetes" + rg_id: 199 + k8ci_id: 4 + state: present + workers: + - name: workgroup1 + labels: + - disktype1=ssd1 + - disktype2=ssd2 + taints: + - key1=value1:NoSchedule + - key2=value2:NoSchedule + annotations: + - node.deckhouse.io/group1=g1 + - node.deckhouse.io/group2=g2 + register: kube diff --git a/examples/basicservices.yaml b/examples/basicservices.yaml new file mode 100644 index 0000000..f5862fa --- /dev/null +++ b/examples/basicservices.yaml @@ -0,0 +1,31 @@ +--- +# +# DECORT vins module example +# + +- hosts: localhost + tasks: + - name: obtain JWT + decort_jwt: + oauth2_url: "https://sso.digitalenergy.online" + validity: 1200 + register: my_jwt + delegate_to: localhost + + - name: print out JWT + debug: + var: my_jwt.jwt + delegate_to: localhost + + - name: Manage bservice at RG + decort_bservice: + account_id: 98 + verify_ssl: false + authenticator: jwt + jwt: "{{ my_jwt.jwt }}" + controller_url: "https://ds1.digitalenergy.online" + rg_id: 1629 + state: present + name: databases + started: True + register: db_bservice diff --git a/examples/cloud-init.yaml b/examples/cloud_init.yaml similarity index 100% rename from examples/cloud-init.yaml rename to examples/cloud_init.yaml diff --git a/library/decort_bservice.py b/library/decort_bservice.py new file mode 100644 index 0000000..5e1270e --- /dev/null +++ b/library/decort_bservice.py @@ -0,0 +1,289 @@ +#!/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: Alexey Dankov (alexey Dankov@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_bservice(DecortController): + def __init__(self,arg_amodule): + super(decort_bservice, self).__init__(arg_amodule) + + validated_acc_id = 0 + validated_rg_id = 0 + validated_rg_facts = None + self.bservice_info = None + if arg_amodule.params['name'] == "" and arg_amodule.params['id'] == 0: + self.result['failed'] = True + self.result['changed'] = False + self.result['msg'] = "Cannot manage Basic Services 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) + + arg_amodule.params['rg_id'] = validated_rg_id + arg_amodule.params['rg_name'] = validated_rg_facts['name'] + self.acc_id = validated_rg_facts['accountId'] + + self.bservice_id,self.bservice_info = self.bservice_find( + self.acc_id, + validated_rg_id, + arg_amodule.params['name'], + arg_amodule.params['id'] + ) + + if self.bservice_id == 0: + self.bservice_should_exist = False + else: + self.bservice_should_exist = True + + def nop(self): + """No operation (NOP) handler for B-service. + 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 B-service ID {} because of its " + "current status '{}'.").format(self.bservice_id, self.bservice_info['status']) + else: + self.result['msg'] = ("No state change to '{}' can be done for " + "non-existent B-service instance.").format(self.amodule.params['state']) + return + + def error(self): + self.result['failed'] = True + self.result['changed'] = False + if self.bservice_id: + self.result['msg'] = ("Invalid target state '{}' requested for B-service ID {} in the " + "current status '{}'.").format(self.bservice_id, + self.amodule.params['state'], + self.bservice_info['status']) + else: + self.result['msg'] = ("Invalid target state '{}' requested for non-existent B-service 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.bservice_id = self.bservice_id = self.bservice_provision( + self.amodule.params['name'], + self.amodule.params['rg_id'], + self.amodule.params['sshuser'], + self.amodule.params['sshkey'] + ) + if self.bservice_id: + _, self.bservice_info = self.bservice_get_by_id(self.bservice_id) + self.bservice_state(self.bservice_info,'enabled',self.amodule.params['started']) + return + + def action(self,d_state,started=False): + self.bservice_state(self.bservice_info,d_state,started) + return + + def restore(self): + + self.result['failed'] = True + self.result['msg'] = "Restore B-Service ID {} manualy.".format(self.bservice_id) + pass + + def destroy(self): + self.bservice_delete(self.bservice_id) + self.bservice_info['status'] = 'DELETED' + self.bservice_should_exist = False + 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 + + ret_dict['id'] = self.bservice_info['id'] + ret_dict['name'] = self.bservice_info['name'] + ret_dict['techStatus'] = self.bservice_info['techStatus'] + ret_dict['state'] = self.bservice_info['status'] + ret_dict['rg_id'] = self.bservice_info['rgId'] + ret_dict['account_id'] = self.acc_id + ret_dict['groupsName'] = self.bservice_info['groupsName'] + ret_dict['groupsIds'] = self.bservice_info['groups'] + return ret_dict + @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), + 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), + state=dict(type='str', + default='present', + choices=['absent', 'disabled', 'enabled', 'present','check']), + started=dict(type='bool', required=False, default=True), + user=dict(type='str', + required=False, + fallback=(env_fallback, ['DECORT_USER'])), + name=dict(type='str', required=True), + sshuser=dict(type='str', required=False,default=None), + sshkey=dict(type='str', required=False,default=None), + id=dict(type='int', required=False, default=0), + rg_id=dict(type='int', default=0), + rg_name=dict(type='str',default=""), + description=dict(type='str', default="Created by decort ansible module"), + 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_bservice.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_bservice(amodule) + + if amodule.params['state'] == 'check': + subj.result['changed'] = False + if subj.bservice_id: + 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 B-service name '{}'. Other arguments are: B-service ID {}, " + "RG name '{}', RG ID {}, Account '{}'.").format(amodule.params['name'], + amodule.params['id'], + amodule.params['rg_name'], + amodule.params['rg_id'], + amodule.params['account_name']) + amodule.fail_json(**subj.result) + pass + + + #MAIN MANAGE PART + + if subj.bservice_id: + if subj.bservice_info['status'] in ("DELETING","DESTROYNG","RECONFIGURING","DESTROYING", + "ENABLING","DISABLING","RESTORING","MODELED"): + subj.error() + elif subj.bservice_info['status'] == "DELETED": + if amodule.params['state'] in ('disabled', 'enabled', 'present'): + subj.restore(subj.bservice_id) + subj.action(amodule.params['state'],amodule.params['started']) + if amodule.params['state'] == 'absent': + subj.nop() + elif subj.bservice_info['techStatus'] in ("STARTED","STOPPED"): + if amodule.params['state'] == 'disabled': + subj.action(amodule.params['state'],amodule.params['started']) + elif amodule.params['state'] == 'absent': + subj.destroy() + else: + subj.action(amodule.params['state'],amodule.params['started']) + elif subj.bservice_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.bservice_info['status'] == "DESTROED": + if amodule.params['state'] in ('present','enabled'): + subj.create() + subj.action(amodule.params['state'],amodule.params['started']) + 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.bservice_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() \ No newline at end of file diff --git a/library/decort_group.py b/library/decort_group.py new file mode 100644 index 0000000..f5ea549 --- /dev/null +++ b/library/decort_group.py @@ -0,0 +1,285 @@ +#!/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: Alexey Dankov (alexey.dankov@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_group(DecortController): + def __init__(self,arg_amodule): + super(decort_group, self).__init__(arg_amodule) + self.group_should_exist = False + validated_bservice_id = None + #find and validate B-Service + + validated_bservice_id, bservice_info = self.bservice_get_by_id(arg_amodule.params['bservice_id']) + if not validated_bservice_id: + self.result['failed'] = True + self.result['changed'] = False + self.result['msg'] = ("Cannot find B-service ID {}.").format(arg_amodule.params['bservice_id']) + self.fail_json(**self.result) + #find group + self.bservice_id = validated_bservice_id + self.bservice_info = bservice_info + self.group_id,self.group_info = self.group_find( + bs_id=validated_bservice_id, + bs_info=bservice_info, + group_id=arg_amodule.params['id'], + group_name=arg_amodule.params['name'], + ) + + if self.group_id: + self.group_should_exist = True + + return + def nop(self): + """No operation (NOP) handler for B-service. + 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.group_id: + self.result['msg'] = ("No state change required for B-service ID {} because of its " + "current status '{}'.").format(self.group_id, self.group_info['status']) + else: + self.result['msg'] = ("No state change to '{}' can be done for " + "non-existent B-service instance.").format(self.amodule.params['state']) + return + + def error(self): + self.result['failed'] = True + self.result['changed'] = False + if self.group_id: + self.result['msg'] = ("Invalid target state '{}' requested for Group ID {} in the " + "current status '{}'.").format(self.group_id, + self.amodule.params['state'], + self.group_info['status']) + else: + self.result['msg'] = ("Invalid target state '{}' requested for non-existent Group name '{}' " + "in B-service {}").format(self.amodule.params['state'], + self.amodule.params['name'], + self.amodule.params['bservice_id'], + ) + return + + def create(self): + + if self.amodule.params['driver'] not in ["KVM_X86","KVM_PPC"]: + self.result['failed'] = True + self.result['msg'] = ("Unsupported driver '{}' is specified for " + "Group.").format(self.amodule.params['driver']) + self.amodule.fail_json(**self.result) + + self.group_id=self.group_provision( + self.bservice_id, + self.amodule.params['name'], + self.amodule.params['count'], + self.amodule.params['cpu'], + self.amodule.params['ram'], + self.amodule.params['boot_disk'], + self.amodule.params['image_id'], + self.amodule.params['driver'], + self.amodule.params['role'], + self.amodule.params['networks'], + self.amodule.params['timeoutStart'], + ) + + if self.amodule.params['state'] in ('started','present'): + self.group_state(self.bservice_id,self.group_id,self.amodule.params['state']) + return + + def action(self): + #change desired state + if ( + self.group_info['techStatus'] == 'STARTED' and self.amodule.params['state'] == 'stopped') or ( + self.group_info['techStatus'] == 'STOPPED' and self.amodule.params['state'] in ('started','present') + ): + self.group_state(self.bservice_id,self.group_id,self.amodule.params['state']) + self.group_resize_count(self.bservice_id,self.group_info,self.amodule.params['count']) + self.group_update_hw( + self.bservice_id, + self.group_info, + self.amodule.params['cpu'], + self.amodule.params['boot_disk'], + self.amodule.params['name'], + self.amodule.params['role'], + self.amodule.params['ram'], + ) + self.group_update_net( + self.bservice_id, + self.group_info, + self.amodule.params['networks'] + ) + return + + def destroy(self): + + self.group_delete( + self.bservice_id, + self.group_id + ) + + 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.result['changed'] == True: + self.group_id,self.group_info = self.group_find( + self.bservice_id, + self.bservice_info, + self.group_id + ) + + ret_dict['account_id'] = self.group_info['accountId'] + ret_dict['rg_id'] = self.group_info['rgId'] + ret_dict['id'] = self.group_info['id'] + ret_dict['name'] = self.group_info['name'] + ret_dict['techStatus'] = self.group_info['techStatus'] + ret_dict['state'] = self.group_info['status'] + ret_dict['Computes'] = self.group_info['computes'] + return ret_dict + @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), + 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), + state=dict(type='str', + default='present', + choices=['absent', 'started', 'stopped', 'present','check']), + 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), + image_id=dict(type='int', required=False), + image_name=dict(type='str', required=False), + driver=dict(type='str', required=False,default="KVM_X86"), + boot_disk=dict(type='int', required=False), + bservice_id=dict(type='int', required=True), + count=dict(type='int', required=True), + timeoutStart=dict(type='int', required=False), + role=dict(type='str', required=False), + cpu=dict(type='int', required=False), + ram=dict(type='int', required=False), + networks=dict(type='list', default=[], required=False), + description=dict(type='str', default="Created by decort ansible module"), + 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_group.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'], + ], + ) + + subj = decort_group(amodule) + + if amodule.params['state'] == 'check': + subj.result['changed'] = False + if subj.group_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 Group name '{}'. " + "B-service ID {}").format(amodule.params['name'], + amodule.params['bservice_id'],) + amodule.fail_json(**subj.result) + + if subj.group_id: + if subj.group_info['status'] in ("DELETING","DESTROYNG","CREATING","DESTROYING", + "ENABLING","DISABLING","RESTORING","MODELED", + "DISABLED","DESTROYED"): + subj.error() + elif subj.group_info['status'] in ("DELETED","DESTROYED"): + if amodule.params['state'] == 'absent': + subj.nop() + if amodule.params['state'] in ('present','started','stopped'): + subj.create() + elif subj.group_info['techStatus'] in ("STARTED","STOPPED"): + if amodule.params['state'] == 'absent': + subj.destroy() + else: + subj.action() + + else: + if amodule.params['state'] == 'absent': + subj.nop() + if amodule.params['state'] in ('present','started','stopped'): + subj.create() + + if subj.result['failed']: + amodule.fail_json(**subj.result) + else: + if subj.group_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() \ No newline at end of file diff --git a/library/decort_k8s.py b/library/decort_k8s.py index 1ed5d55..3abb4e4 100644 --- a/library/decort_k8s.py +++ b/library/decort_k8s.py @@ -80,7 +80,7 @@ class decort_k8s(DecortController): if self.k8s_id: self.k8s_should_exist = True self.acc_id = self.k8s_info['accountId'] - # check workers and groups for add or remove + # check workers and groups for add or remove? return @@ -147,17 +147,13 @@ class decort_k8s(DecortController): 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['workers'][0], self.amodule.params['extnet_id'], self.amodule.params['with_lb'], self.amodule.params['description'],) diff --git a/library/decort_vins.py b/library/decort_vins.py index 924eb4f..d84635b 100644 --- a/library/decort_vins.py +++ b/library/decort_vins.py @@ -242,6 +242,7 @@ facts: from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback +import paramiko from ansible.module_utils.decort_utils import * @@ -316,6 +317,9 @@ def decort_vins_parameters(): ext_net_id=dict(type='int', required=False, default=-1), ext_ip_addr=dict(type='str', required=False, default=''), ipcidr=dict(type='str', required=False, default=''), + mgmtaddr=dict(type='str',required=False, default=''), + custom_config=dict(type='bool',required=False, default=False), + config_save=dict(type='bool',required=False, default=False), jwt=dict(type='str', required=False, fallback=(env_fallback, ['DECORT_JWT']), @@ -387,6 +391,7 @@ def main(): 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" @@ -443,7 +448,6 @@ def main(): # rg_name without account specified decon.result['msg'] = "Cannot find ViNS by name when RG name is empty and RG ID is 0." decon.fail_json(**decon.result) - # # Initial validation of module arguments is complete # @@ -457,7 +461,7 @@ def main(): # # "MODELED", "CREATED", "ENABLED", "ENABLING", "DISABLED", "DISABLING", "DELETED", "DELETING", "DESTROYED", "DESTROYING" # - + # if cconfig_save is true, only config save without other updates vins_should_exist = False if vins_id: @@ -491,7 +495,9 @@ def main(): elif amodule.params['state'] in ('present', 'enabled'): # update ViNS decon.vins_update(vins_facts, - amodule.params['ext_net_id'], amodule.params['ext_ip_addr']) + amodule.params['ext_net_id'], amodule.params['ext_ip_addr'], + amodule.params['mgmtaddr'], + ) elif amodule.params['state'] == 'disabled': # disable and update ViNS decon.vins_state(vins_facts, 'disabled') @@ -586,6 +592,10 @@ def main(): # be returned. _, vins_facts = decon.vins_find(vins_id) decon.result['facts'] = decort_vins_package_facts(vins_facts, amodule.check_mode) + # add password to facts if mgmtaddr is present + # need reworking + if amodule.params['mgmtaddr'] != "": + decon.result['facts'].update({'password': vins_facts['VNFDev']['config']['mgmt']['password']}) amodule.exit_json(**decon.result) diff --git a/module_utils/decort_utils.py b/module_utils/decort_utils.py index e08c7ba..fd8bb7d 100644 --- a/module_utils/decort_utils.py +++ b/module_utils/decort_utils.py @@ -2146,10 +2146,10 @@ class DecortController(object): expected_state = "" if vins_dict['status'] in ["CREATED", "ENABLED"] and desired_state == 'disabled': - rgstate_api = "/restmachine/cloudapi/vins/disable" + vinsstate_api = "/restmachine/cloudapi/vins/disable" expected_state = "DISABLED" elif vins_dict['status'] == "DISABLED" and desired_state == 'enabled': - rgstate_api = "/restmachine/cloudapi/vins/enable" + vinsstate_api = "/restmachine/cloudapi/vins/enable" expected_state = "ENABLED" if vinsstate_api != "": @@ -2166,7 +2166,7 @@ class DecortController(object): desired_state) return - def vins_update(self, vins_dict, ext_net_id, ext_ip_addr=""): + def vins_update(self, vins_dict, ext_net_id, ext_ip_addr="", mgmtaddr=""): """Update ViNS. Currently only updates to the external network connection settings and external IP address assignment are implemented. Note that as ViNS created at account level cannot have external connections, attempt @@ -2191,6 +2191,11 @@ class DecortController(object): self.result['msg'] = ("vins_update() in check mode: updating ViNS ID {}, name '{}' " "was requested.").format(vins_dict['id'], vins_dict['name']) return + if self.amodule.params['config_save'] and vins_dict['VNFDev']['customPrecfg']: + # only save config,no other modifictaion + self.result['changed'] = True + self._vins_vnf_config_save(vins_dict['VNFDev']['id']) + return if not vins_dict['rgId']: # this ViNS exists at account level - no updates are possible @@ -2242,7 +2247,78 @@ class DecortController(object): "no reconnection to default network will be done.").format(vins_dict['id'], gw_config[ 'ext_net_id']) + for iface in vins_dict['VNFDev']['interfaces']: + if iface['ipAddress'] == mgmtaddr: + if not iface['listenSsh']: + self._vins_vnf_addmgmtaddr(vins_dict['VNFDev']['id'],mgmtaddr) + elif mgmtaddr =="": + if iface['listenSsh'] and iface['name'] != "ens9": + self._vins_vnf_delmgmtaddr(vins_dict['VNFDev']['id'],iface['ipAddress']) + + if self.amodule.params['custom_config']: + if not vins_dict['VNFDev']['customPrecfg']: + self._vins_vnf_config_save(vins_dict['VNFDev']['id']) + self._vins_vnf_customconfig_set(vins_dict['VNFDev']['id']) + else: + if vins_dict['VNFDev']['customPrecfg']: + self._vins_vnf_customconfig_set(vins_dict['VNFDev']['id'],False) + + return + def _vins_vnf_addmgmtaddr(self,dev_id,mgmtip): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_vnf_addmgmtaddr") + api_params = dict(devId=dev_id,ip=mgmtip) + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/addMgmtAddr", api_params) + if api_resp.status_code == 200: + self.result['changed'] = True + self.result['failed'] = False + else: + self.result['warning'] = ("_vins_vnf_addmgmtaddr(): failed to add MGMT addr VNFID {} iface ADDR {}. HTTP code {}, " + "response {}.").format(dev_id,mgmtip,api_resp.status_code, api_resp.reason) + return + + def _vins_vnf_delmgmtaddr(self,dev_id,mgmtip): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_vnf_delmgmtaddr") + api_params = dict(devId=dev_id,ip=mgmtip) + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/delMgmtAddr", api_params) + if api_resp.status_code == 200: + self.result['changed'] = True + self.result['failed'] = False + else: + self.result['warning'] = ("_vins_vnf_delmgmtaddr(): failed to delete MGMT addr VNFID {} iface ADDR {}. HTTP code {}, " + "response {}.").format(dev_id,mgmtip,api_resp.status_code, api_resp.reason) + return + + def _vins_vnf_customconfig_set(self,dev_id,arg_mode=True): + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_vnf_customconfig_set") + api_params = dict(devId=dev_id,mode=arg_mode) + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/customSet", api_params) + if api_resp.status_code == 200: + self.result['changed'] = True + self.result['failed'] = False + else: + self.result['warning'] = ("_vins_vnf_customconfig_set(): failed to enable or disable Custom pre-config mode on the VNF device. {}. HTTP code {}, " + "response {}.").format(dev_id,api_resp.status_code, api_resp.reason) + return + + def _vins_vnf_config_save(self,dev_id): + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "vins_vnf_config_save") + api_params = dict(devId=dev_id) + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudbroker/vnfdev/configSave", api_params) + if api_resp.status_code == 200: + self.result['changed'] = True + self.result['failed'] = False + else: + self.result['warning'] = ("_vins_vnf_config_set(): failed to Save configuration on the VNF device. {}. HTTP code {}, " + "response {}.").format(dev_id,api_resp.status_code, api_resp.reason) + return + + def vins_vnf_ifaceadd(self): + return + + def vins_vnf_ifaceremove(self): return ############################## @@ -2891,12 +2967,9 @@ class DecortController(object): return def k8s_provision(self, k8s_name, - wg_name, k8ci_id, - rg_id, master_count, + k8ci_id,rg_id, master_count, master_cpu, master_ram, - master_disk, worker_count, - worker_cpu, worker_ram, - worker_disk, extnet_id, + master_disk, default_worker, extnet_id, with_lb, annotation, ): self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "k8s_provision") @@ -2906,20 +2979,31 @@ class DecortController(object): self.result['msg'] = ("k8s_provision() in check mode. Provision k8s '{}' in RG ID {} " "was requested.").format(k8s_name, rg_id) return 0 + def_wg_name = default_worker['name'] + def_wg_count = default_worker['num'] + def_wg_cpu = default_worker['cpu'] + def_wg_ram = default_worker['ram'] + def_wg_disk = default_worker['disk'] + def_wg_lab = default_worker['labels'] if "labels" in default_worker else None + def_wg_taints = default_worker['taints'] if "taints" in default_worker else None + def_wg_ann = default_worker['annotations'] if "annotations" in default_worker else None api_url = "/restmachine/cloudapi/k8s/create" api_params = dict(name=k8s_name, rgId=rg_id, k8ciId=k8ci_id, - workerGroupName=wg_name, + workerGroupName=def_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, + workerNum=def_wg_count, + workerCpu=def_wg_cpu, + workerRam=def_wg_ram, + workerDisk=def_wg_disk, + labels=def_wg_lab, + taints=def_wg_taints, + annotations=def_wg_ann, extnetId=extnet_id, withLB=with_lb, desc=annotation, @@ -2981,7 +3065,7 @@ class DecortController(object): for rec_inn in arg_k8swg['k8sGroups']['workers']: for rec_out in arg_modwg: - if rec_inn['num'] != rec_out['num']: + if rec_inn['num'] != rec_out['num'] and rec_out['num'] != 0: count = rec_inn['num']-rec_out['num'] cmp_list = [] if count > 0: @@ -3004,6 +3088,9 @@ class DecortController(object): workerCpu=wg['cpu'], workerRam=wg['ram'], workerDisk=wg['disk'], + labels=wg['labels'] if "labels" in wg else None, + taints=wg['taints'] if "taints" in wg else None, + annotations=wg['annotations'] if "annotations" in wg else None, ) api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/k8s/workersGroupAdd", api_params) self.result['changed'] = True @@ -3055,3 +3142,347 @@ class DecortController(object): ret_conf = api_resp.content.decode('utf8') return ret_conf + ############################## + # + # Bservice management + # + ############################## + def bservice_get_by_id(self,bs_id): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "bservice_get_by_id") + + ret_bs_id = 0 + ret_bs_dict = dict() + + if not bs_id: + self.result['failed'] = True + self.result['msg'] = "bservice_get_by_id(): zero B-Service ID specified." + self.amodule.fail_json(**self.result) + + api_params = dict(serviceId=bs_id, ) + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/bservice/get", api_params) + if api_resp.status_code == 200: + ret_bs_id = bs_id + ret_bs_dict = json.loads(api_resp.content.decode('utf8')) + else: + self.result['warning'] = ("bservice_get_by_id(): failed to get B-service by ID {}. HTTP code {}, " + "response {}.").format(bs_id, api_resp.status_code, api_resp.reason) + + return ret_bs_id, ret_bs_dict + def _bservice_rg_list(self,acc_id,rg_id): + ret_bs_dict=dict() + api_params = dict(accountId=acc_id,rgId=rg_id ) + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/bservice/list", api_params) + if api_resp.status_code == 200: + ret_bs_dict = json.loads(api_resp.content.decode('utf8')) + else: + self.result['warning'] = ("bservice_rg_list(): failed to get B-service list. HTTP code {}, " + "response {}.").format(api_resp.status_code, api_resp.reason) + return ret_bs_dict + + def bservice_find(self,account_id,rg_id,bservice_name="",bservice_id = 0,check_state=True): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "bservice_find") + + if bservice_id == 0: + bs_rg_list = self._bservice_rg_list(account_id,rg_id) + for srv in bs_rg_list: + if bservice_name == srv['name']: + bservice_id = int(srv['id']) + + if bservice_id > 0: + ret_bs_id,ret_bs_dict = self.bservice_get_by_id(bservice_id) + return ret_bs_id,ret_bs_dict + else: + return bservice_id,None + + def bservice_provision(self,bs_name,rgid,sshuser=None,sshkey=None): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "bservice_provision") + + api_url = "/restmachine/cloudapi/bservice/create" + api_params = dict( + name = bs_name, + rgId = rgid, + sshUser = sshuser, + sshKey = sshkey, + ) + api_resp = self.decort_api_call(requests.post, api_url, api_params) + self.result['failed'] = False + self.result['changed'] = True + ret_bservice_id = int(api_resp.content.decode('utf8')) + return ret_bservice_id + + def bservice_state(self,bs_dict,desired_state,started=True): + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "bservice_state") + + if desired_state == "present": + desired_state = "enabled" + + NOP_STATES_FOR_BS_CHANGE = ["MODELED", "DISABLING", "ENABLING", "DELETING", "DELETED", "DESTROYING", + "DESTROYED","RESTORYNG","RECONFIGURING"] + VALID_TARGET_STATES = ["enabled", "disabled"] + + if bs_dict['status'] in NOP_STATES_FOR_BS_CHANGE: + self.result['failed'] = False + self.result['msg'] = ("bservice_state(): no state change possible for ViNS ID {} " + "in its current state '{}'.").format(bs_dict['id'], bs_dict['status']) + return + + if desired_state not in VALID_TARGET_STATES: + self.result['failed'] = False + self.result['warning'] = ("bservice_state(): unrecognized desired state '{}' requested " + "for B-service ID {}. No B-service state change will be done.").format(desired_state, + bs_dict['id']) + return + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['msg'] = ("bservice_state() in check mode: setting state of B-service ID {}, name '{}' to " + "'{}' was requested.").format(bs_dict['id'], bs_dict['name'], + desired_state) + return + + bsstate_api = "" # this string will also be used as a flag to indicate that API call is necessary + api_params = dict(serviceId=bs_dict['id']) + expected_state = "" + + if bs_dict['status'] in ["CREATED", "ENABLED"] and desired_state == 'disabled': + bsstate_api = "/restmachine/cloudapi/bservice/disable" + expected_state = "DISABLED" + elif bs_dict['status'] in ["CREATED", "DISABLED"] and desired_state == 'enabled': + bsstate_api = "/restmachine/cloudapi/bservice/enable" + expected_state = "ENABLED" + + if bsstate_api != "": + self.decort_api_call(requests.post, bsstate_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 + bs_dict['status'] = expected_state + else: + self.result['failed'] = False + self.result['msg'] = ("bservice_state(): no state change required for B-service ID {} from current " + "state '{}' to desired state '{}'.").format(bs_dict['id'], + bs_dict['status'], + desired_state) + + start_api = "" + + if bs_dict['status'] in ["ENABLED","enabled"]: + if started == True and bs_dict['techStatus'] == "STOPPED": + start_api = "/restmachine/cloudapi/bservice/start" + t_state_expected = "STARTED" + if started == False and bs_dict['techStatus'] == "STARTED": + start_api = "/restmachine/cloudapi/bservice/stop" + t_state_expected = "STOPPED" + + if start_api != "": + self.decort_api_call(requests.post, start_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 + bs_dict['techStatus'] = t_state_expected + else: + self.result['failed'] = False + self.result['msg'] = ("bservice_state(): no start/stop action required for B-service ID {} from current " + "techStatus '{}' to desired started = '{}'.").format(bs_dict['id'], + bs_dict['techStatus'], + started) + return + + def bservice_delete(self,bs_id,permanently=True): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "bservice_delete") + + if self.amodule.check_mode: + self.result['failed'] = False + self.result['msg'] = "bservice_delete() in check mode: delete B-Service ID {} was requested.".format(bs_id) + return + + api_params = dict(serviceId=bs_id,permanently=permanently) + self.decort_api_call(requests.post, "/restmachine/cloudapi/bservice/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'] = "bservice_delete() B-Service ID {} was deleted.".format(bs_id) + self.result['changed'] = True + + return + +# +# GROUP MANAGE +# + def _group_get_by_id(self,bs_id,g_id): + + api_params = dict(serviceId=bs_id,compgroupId=g_id) + api_resp = self.decort_api_call(requests.post, "/restmachine/cloudapi/bservice/groupGet", api_params) + if api_resp.status_code == 200: + ret_gr_id = g_id + ret_gr_dict = json.loads(api_resp.content.decode('utf8')) + else: + self.result['warning'] = ("group_get_by_id(): failed to get Group by ID {}. HTTP code {}, " + "response {}.").format(g_id, api_resp.status_code, api_resp.reason) + return ret_gr_id,ret_gr_dict + + def group_find(self,bs_id,bs_info,group_id=0,group_name=""): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_find") + + if group_id == 0: + try: + i = bs_info['groupsName'].index(group_name) + except: + return 0,None + group_id = int(bs_info['groups'][i]) + return self._group_get_by_id(bs_id,group_id) + + def group_state(self,bs_id,gr_id,desired_state): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_state") + + group_api="" + if desired_state == 'stopped': + group_api = "/restmachine/cloudapi/bservice/groupStop" + state_expected = "STOPPED" + else: + group_api = "/restmachine/cloudapi/bservice/groupStart" + state_expected = "STARTED" + api_params = dict( + serviceId=bs_id, + compgroupId=gr_id + ) + if group_api != "": + self.decort_api_call(requests.post, group_api, api_params) + # On success the above call will return here. On error it will abort execution by calling fail_json. + self.result['failed'] = False + self.result['changed'] = True + else: + self.result['failed'] = False + self.result['msg'] = ("group_state(): no start/stop action required for B-service ID {} " + "to desired state '{}'.").format(bs_id,desired_state) + return + def group_resize_count(self,bs_id,gr_dict,desired_count): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_resize_count") + + count = len(gr_dict['computes']) + if desired_count != count: + api_params=dict( + serviceId=bs_id, + compgroupId=gr_dict['id'], + count=desired_count, + mode="ABSOLUTE" + ) + api_url = "/restmachine/cloudapi/bservice/groupResize" + self.decort_api_call(requests.post, api_url, api_params) + self.result['failed'] = False + self.result['changed'] = True + self.need_update_group_info = True + else: + self.result['failed'] = False + self.result['msg'] = ("group_resize_count(): no need resize Group ID {}.").format(gr_dict['id']) + return + def group_update_hw(self,bs_id,gr_dict,arg_cpu,arg_disk,arg_name,arg_role,arg_ram): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_update_hw") + + api_params=dict( + serviceId=bs_id, + compgroupId=gr_dict['id'], + force=True, + ) + + if gr_dict['cpu'] != arg_cpu: + api_params.update({'cpu': arg_cpu}) + if gr_dict['ram'] != arg_ram: + api_params.update({'ram': arg_ram}) + if gr_dict['role'] != arg_role: + api_params.update({'role': arg_role}) + if gr_dict['disk'] != arg_disk: + api_params.update({'disk': arg_disk}) + if gr_dict['name'] != arg_name: + api_params.update({'name': arg_name}) + + api_url = "/restmachine/cloudapi/bservice/groupUpdate" + if len(api_params) > 3: + # + self.decort_api_call(requests.post, api_url, api_params) + self.result['failed'] = False + self.result['changed'] = True + self.need_update_group_info = True + else: + self.result['failed'] = False + self.result['msg'] = ("group_update_hw(): no need update Group ID {}.").format(gr_dict['id']) + return + + def group_update_net(self,bs_id,gr_dict,arg_net): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_update_net") + + list_vins= list() + list_extnet= list() + for net in arg_net: + if net['type'] == 'VINS': + list_vins.append(net['id']) + else: + list_extnet.append(net['id']) + + if gr_dict['vinses'] != list_vins: + api_url = "/restmachine/cloudapi/bservice/groupUpdateVins" + api_params = dict( + serviceId=bs_id, + compgroupId=gr_dict['id'], + vinses=list_vins + ) + self.decort_api_call(requests.post, api_url, api_params) + self.result['failed'] = False + self.result['changed'] = True + + #extnet connection need stoped status of group + #rly need connect group to extnet ? + return + def group_provision( + self,bs_id,arg_name,arg_count=1,arg_cpu=1,arg_ram=1024, + arg_boot_disk=10,arg_image_id=0,arg_driver="KVM_X86",arg_role="", + arg_network=None,arg_timeout=0 + ): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_provision") + + list_vins= list() + for net in arg_network: + if net['type'] == 'VINS': + list_vins.append(net['id']) + api_url = "/restmachine/cloudapi/bservice/groupAdd" + api_params = dict( + serviceId = bs_id, + name = arg_name, + count = arg_count, + cpu = arg_cpu, + ram = arg_ram, + disk = arg_boot_disk, + imageId = arg_image_id, + driver = arg_driver, + role = arg_role, + vinses = list_vins, + timeoutStart = arg_timeout + ) + self.decort_api_call(requests.post, api_url, api_params) + self.result['failed'] = False + self.result['changed'] = True + return + + def group_delete(self,bs_id,gr_id): + + self.result['waypoints'] = "{} -> {}".format(self.result['waypoints'], "group_delete") + + api_url = "/restmachine/cloudapi/bservice/groupRemove" + api_params=dict( + serviceId = bs_id, + compgroupId = gr_id + ) + self.decort_api_call(requests.post, api_url, api_params) + self.result['failed'] = False + self.result['msg'] = "group_delete() Group ID {} was deleted.".format(gr_id) + self.result['changed'] = True + return