#!/usr/bin/python DOCUMENTATION = r''' --- module: decort_k8s description: See L(Module Documentation,https://repository.basistech.ru/BASIS/decort-ansible/wiki/Home). ''' from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback from ansible.module_utils.decort_utils import * from copy import deepcopy class decort_k8s(DecortController): def __init__(self): super(decort_k8s, self).__init__(AnsibleModule(**self.amodule_init_args)) arg_amodule = self.amodule validated_acc_id = 0 validated_rg_id = 0 validated_rg_facts = None validated_k8ci_id = 0 self.k8s_should_exist = False self.wg_default_params = { 'num': 1, 'cpu': 1, 'ram': 1024, 'labels': [], 'taints': [], 'annotations': [], 'ci_user_data': {}, 'chipset': 'i440fx', } if arg_amodule.params['name'] == "" and arg_amodule.params['id'] is None: 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.amodule.fail_json(**self.result) if arg_amodule.params['id'] is None: 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.amodule.fail_json(**self.result) # fail the module -> exit # now validate RG validated_rg_id, validated_rg_facts = self.rg_find( arg_account_id=validated_acc_id, arg_rg_id=arg_amodule.params['rg_id'], arg_rg_name=arg_amodule.params['rg_name'] ) 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.amodule.fail_json(**self.result) # fail the module - exit 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'] 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? if not self.k8s_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.amodule.fail_json(**self.result) if not arg_amodule.params['workers']: self.result['failed'] = True self.result['changed'] = False self.result['msg'] = "At least one worker group must be present" self.amodule.fail_json(**self.result) return def package_facts(self,check_mode=False): ret_dict = dict( name="", state="CHECK_MODE", account_id=0, rg_id=0, config=None, ) if self.amodule.params['getConfig'] and self.k8s_info['techStatus'] == "STARTED": ret_dict['config'] = self.k8s_getConfig() 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.k8s_info['rgId'] ret_dict['vins_id'] = self.k8s_vins_id ret_dict['account_id'] = self.acc_id ret_dict['k8s_Masters'] = self.k8s_info['k8sGroups']['masters'] ret_dict['k8s_Workers'] = self.k8s_info['k8sGroups']['workers'] return ret_dict def nop(self): """No operation (NOP) handler for k8s cluster management by decort_k8s 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 k8s cluster 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): master_chipset = self.amodule.params['master_chipset'] if master_chipset is None: master_chipset = 'i440fx' target_wgs = deepcopy(self.amodule.params['workers']) for wg in target_wgs: for param, default_value in self.wg_default_params.items(): if wg[param] is None: wg[param] = default_value k8s_id = self.k8s_provision(self.amodule.params['name'], self.amodule.params['k8ci_id'], self.amodule.params['rg_id'], self.amodule.params['vins_id'], self.amodule.params['network_plugin'], self.amodule.params['master_count'], self.amodule.params['master_cpu'], self.amodule.params['master_ram'], self.amodule.params['master_disk'], self.amodule.params['master_sepid'], self.amodule.params['master_pool'], target_wgs[0], self.amodule.params['extnet_id'], self.amodule.params['with_lb'], self.amodule.params['ha_lb'], self.amodule.params['additionalSANs'], self.amodule.params['init_conf'], self.amodule.params['cluster_conf'], self.amodule.params['kublet_conf'], self.amodule.params['kubeproxy_conf'], self.amodule.params['join_conf'], self.amodule.params['oidc_cert'], self.amodule.params['description'], self.amodule.params['extnet_only'], master_chipset, ) if not k8s_id: if k8s_id == 0: return else: self.result['failed'] = True self.amodule.fail_json(**self.result) self.k8s_id,self.k8s_info = self.k8s_find(k8s_id=k8s_id, k8s_name=self.amodule.params['name'], rg_id=self.rg_id, check_state=False) if self.k8s_id: self.k8s_should_exist = True self.k8s_workers_modify( arg_k8swg=self.k8s_info, arg_modwg=target_wgs, ) return def destroy(self): self.k8s_delete(self.k8s_id,self.amodule.params['permanent']) self.k8s_info['status'] = 'DELETED' self.k8s_should_exist = False return def action(self, disared_state, started=True, preupdate: bool = False): if self.amodule.params['master_chipset'] is not None: self.result['msg'] = ( '"master_chipset" parameter must not be specified ' 'when modifying an existing K8s cluster.' ) self.exit(fail=True) if preupdate: # K8s info updating self.k8s_info = self.k8s_get_by_id(k8s_id=self.k8s_id) #k8s state self.k8s_state(self.k8s_info, disared_state, started) self.k8s_info = self.k8s_get_by_id(k8s_id=self.k8s_id) if started == True and self.k8s_info['techStatus'] == "STOPPED": self.k8s_state(self.k8s_info, disared_state,started) self.k8s_info['techStatus'] == "STARTED" #check groups and modify if needed if self.aparams['workers'] is not None: self.k8s_workers_modify(self.k8s_info, self.amodule.params['workers']) if self.result['changed'] == True: self.k8s_info = self.k8s_get_by_id(k8s_id=self.k8s_id) #TODO check workers metadata and modify if needed return @property def amodule_init_args(self) -> dict: return self.pack_amodule_init_args( argument_spec=dict( account_id=dict( type='int', ), account_name=dict( type='str', default='', ), quotas=dict( type='dict', ), state=dict( type='str', default='present', choices=[ 'absent', 'disabled', 'enabled', 'present', 'check', ], ), permanent=dict( type='bool', default=False, ), started=dict( type='bool', default=True, ), name=dict( type='str', default='', ), id=dict( type='int', ), getConfig=dict( type='bool', default=False, ), rg_id=dict( type='int', default=0, ), rg_name=dict( type='str', default='', ), vins_id=dict( type='int', ), k8ci_id=dict( type='int', ), network_plugin=dict( type='str', default='flannel', ), master_count=dict( type='int', default=1, ), master_cpu=dict( type='int', default=2, ), master_ram=dict( type='int', default=2048, ), master_disk=dict( type='int', default=10, ), master_sepid=dict( type='int', ), master_pool=dict( type='str', ), workers=dict( type='list', elements='dict', options=dict( name=dict( type='str', required=True, ), num=dict( type='int', ), cpu=dict( type='int', ), ram=dict( type='int', ), disk=dict( type='int', ), sep_id=dict( type='int', ), pool=dict( type='str', ), annotations=dict( type='list', elements='str', ), ci_user_data=dict( type='dict', ), labels=dict( type='list', elements='str', ), taints=dict( type='list', elements='str', ), chipset=dict( type='str', choices=['Q35', 'i440fx'], ), ), ), workers_metadata=dict( type='bool', default=False, ), extnet_id=dict( type='int', default=0, ), description=dict( type='str', default='Created by decort ansible module', ), with_lb=dict( type='bool', default=True, ), ha_lb=dict( type='bool', default=False, ), extnet_only=dict( type='bool', default=False, ), additionalSANs=dict( type='list', ), init_conf=dict( type='dict', ), cluster_conf=dict( type='dict', ), kublet_conf=dict( type='dict', ), kubeproxy_conf=dict( type='dict', ), join_conf=dict( type='dict', ), oidc_cert=dict( type='raw', ), master_chipset=dict( type='str', choices=['Q35', 'i440fx'], ), ), supports_check_mode=True, required_one_of=[ ('id', 'name'), ], ) def main(): subj = decort_k8s() amodule = subj.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(disared_state=amodule.params['state'], preupdate=True) if amodule.params['state'] == 'absent': if amodule.params['permanent']: subj.destroy() else: 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()