# pylint: skip-file # flake8: noqa class RouterException(Exception): ''' Router exception''' pass class RouterConfig(OpenShiftCLIConfig): ''' RouterConfig is a DTO for the router. ''' def __init__(self, rname, namespace, kubeconfig, router_options): super(RouterConfig, self).__init__(rname, namespace, kubeconfig, router_options) class Router(OpenShiftCLI): ''' Class to wrap the oc command line tools ''' def __init__(self, router_config, verbose=False): ''' Constructor for OpenshiftOC a router consists of 3 or more parts - dc/router - svc/router - sa/router - secret/router-certs - clusterrolebinding/router-router-role ''' super(Router, self).__init__('default', router_config.kubeconfig, verbose) self.config = router_config self.verbose = verbose self.router_parts = [{'kind': 'dc', 'name': self.config.name}, {'kind': 'svc', 'name': self.config.name}, {'kind': 'sa', 'name': self.config.config_options['service_account']['value']}, {'kind': 'secret', 'name': self.config.name + '-certs'}, {'kind': 'clusterrolebinding', 'name': 'router-' + self.config.name + '-role'}, ] self.__prepared_router = None self.dconfig = None self.svc = None self._secret = None self._serviceaccount = None self._rolebinding = None @property def prepared_router(self): ''' property for the prepared router''' if self.__prepared_router is None: results = self._prepare_router() if not results or 'returncode' in results and results['returncode'] != 0: if 'stderr' in results: raise RouterException('Could not perform router preparation: %s' % results['stderr']) raise RouterException('Could not perform router preparation.') self.__prepared_router = results return self.__prepared_router @prepared_router.setter def prepared_router(self, obj): '''setter for the prepared_router''' self.__prepared_router = obj @property def deploymentconfig(self): ''' property deploymentconfig''' return self.dconfig @deploymentconfig.setter def deploymentconfig(self, config): ''' setter for property deploymentconfig ''' self.dconfig = config @property def service(self): ''' property for service ''' return self.svc @service.setter def service(self, config): ''' setter for property service ''' self.svc = config @property def secret(self): ''' property secret ''' return self._secret @secret.setter def secret(self, config): ''' setter for property secret ''' self._secret = config @property def serviceaccount(self): ''' property for serviceaccount ''' return self._serviceaccount @serviceaccount.setter def serviceaccount(self, config): ''' setter for property serviceaccount ''' self._serviceaccount = config @property def rolebinding(self): ''' property rolebinding ''' return self._rolebinding @rolebinding.setter def rolebinding(self, config): ''' setter for property rolebinding ''' self._rolebinding = config def get_object_by_kind(self, kind): '''return the current object kind by name''' if re.match("^(dc|deploymentconfig)$", kind, flags=re.IGNORECASE): return self.deploymentconfig elif re.match("^(svc|service)$", kind, flags=re.IGNORECASE): return self.service elif re.match("^(sa|serviceaccount)$", kind, flags=re.IGNORECASE): return self.serviceaccount elif re.match("secret", kind, flags=re.IGNORECASE): return self.secret elif re.match("clusterrolebinding", kind, flags=re.IGNORECASE): return self.rolebinding return None def get(self): ''' return the self.router_parts ''' self.service = None self.deploymentconfig = None self.serviceaccount = None self.secret = None self.rolebinding = None for part in self.router_parts: result = self._get(part['kind'], rname=part['name']) if result['returncode'] == 0 and part['kind'] == 'dc': self.deploymentconfig = DeploymentConfig(result['results'][0]) elif result['returncode'] == 0 and part['kind'] == 'svc': self.service = Service(content=result['results'][0]) elif result['returncode'] == 0 and part['kind'] == 'sa': self.serviceaccount = ServiceAccount(content=result['results'][0]) elif result['returncode'] == 0 and part['kind'] == 'secret': self.secret = Secret(content=result['results'][0]) elif result['returncode'] == 0 and part['kind'] == 'clusterrolebinding': self.rolebinding = RoleBinding(content=result['results'][0]) return {'deploymentconfig': self.deploymentconfig, 'service': self.service, 'serviceaccount': self.serviceaccount, 'secret': self.secret, 'clusterrolebinding': self.rolebinding, } def exists(self): '''return a whether svc or dc exists ''' if self.deploymentconfig and self.service and self.secret and self.serviceaccount: return True return False def delete(self): '''return all pods ''' parts = [] for part in self.router_parts: parts.append(self._delete(part['kind'], part['name'])) rval = 0 for part in parts: if part['returncode'] != 0 and not 'already exist' in part['stderr']: rval = part['returncode'] return {'returncode': rval, 'results': parts} def add_modifications(self, deploymentconfig): '''modify the deployment config''' # We want modifications in the form of edits coming in from the module. # Let's apply these here edit_results = [] for edit in self.config.config_options['edits'].get('value', []): if edit['action'] == 'put': edit_results.append(deploymentconfig.put(edit['key'], edit['value'])) if edit['action'] == 'update': edit_results.append(deploymentconfig.update(edit['key'], edit['value'], edit.get('index', None), edit.get('curr_value', None))) if edit['action'] == 'append': edit_results.append(deploymentconfig.append(edit['key'], edit['value'])) if edit_results and not any([res[0] for res in edit_results]): return None return deploymentconfig # pylint: disable=too-many-branches def _prepare_router(self): '''prepare router for instantiation''' # if cacert, key, and cert were passed, combine them into a pem file if (self.config.config_options['cacert_file']['value'] and self.config.config_options['cert_file']['value'] and self.config.config_options['key_file']['value']): router_pem = '/tmp/router.pem' with open(router_pem, 'w') as rfd: rfd.write(open(self.config.config_options['cert_file']['value']).read()) rfd.write(open(self.config.config_options['key_file']['value']).read()) if self.config.config_options['cacert_file']['value'] and \ os.path.exists(self.config.config_options['cacert_file']['value']): rfd.write(open(self.config.config_options['cacert_file']['value']).read()) atexit.register(Utils.cleanup, [router_pem]) self.config.config_options['default_cert']['value'] = router_pem elif self.config.config_options['default_cert']['value'] is None: # No certificate was passed to us. do not pass one to oc adm router self.config.config_options['default_cert']['include'] = False options = self.config.to_option_list() cmd = ['router', self.config.name] cmd.extend(options) cmd.extend(['--dry-run=True', '-o', 'json']) results = self.openshift_cmd(cmd, oadm=True, output=True, output_type='json') # pylint: disable=maybe-no-member if results['returncode'] != 0 or 'items' not in results['results']: return results oc_objects = {'DeploymentConfig': {'obj': None, 'path': None, 'update': False}, 'Secret': {'obj': None, 'path': None, 'update': False}, 'ServiceAccount': {'obj': None, 'path': None, 'update': False}, 'ClusterRoleBinding': {'obj': None, 'path': None, 'update': False}, 'Service': {'obj': None, 'path': None, 'update': False}, } # pylint: disable=invalid-sequence-index for res in results['results']['items']: if res['kind'] == 'DeploymentConfig': oc_objects['DeploymentConfig']['obj'] = DeploymentConfig(res) elif res['kind'] == 'Service': oc_objects['Service']['obj'] = Service(res) elif res['kind'] == 'ServiceAccount': oc_objects['ServiceAccount']['obj'] = ServiceAccount(res) elif res['kind'] == 'Secret': oc_objects['Secret']['obj'] = Secret(res) elif res['kind'] == 'ClusterRoleBinding': oc_objects['ClusterRoleBinding']['obj'] = RoleBinding(res) # Currently only deploymentconfig needs updating # Verify we got a deploymentconfig if not oc_objects['DeploymentConfig']['obj']: return results # add modifications added oc_objects['DeploymentConfig']['obj'] = self.add_modifications(oc_objects['DeploymentConfig']['obj']) for oc_type, oc_data in oc_objects.items(): if oc_data['obj'] is not None: oc_data['path'] = Utils.create_tmp_file_from_contents(oc_type, oc_data['obj'].yaml_dict) return oc_objects def create(self): '''Create a router This includes the different parts: - deploymentconfig - service - serviceaccount - secrets - clusterrolebinding ''' results = [] self.needs_update() import time # pylint: disable=maybe-no-member for kind, oc_data in self.prepared_router.items(): if oc_data['obj'] is not None: time.sleep(1) if self.get_object_by_kind(kind) is None: results.append(self._create(oc_data['path'])) elif oc_data['update']: results.append(self._replace(oc_data['path'])) rval = 0 for result in results: if result['returncode'] != 0 and not 'already exist' in result['stderr']: rval = result['returncode'] return {'returncode': rval, 'results': results} def update(self): '''run update for the router. This performs a replace''' results = [] # pylint: disable=maybe-no-member for _, oc_data in self.prepared_router.items(): if oc_data['update']: results.append(self._replace(oc_data['path'])) rval = 0 for result in results: if result['returncode'] != 0: rval = result['returncode'] return {'returncode': rval, 'results': results} # pylint: disable=too-many-return-statements,too-many-branches def needs_update(self): ''' check to see if we need to update ''' # ServiceAccount: # Need to determine changes from the pregenerated ones from the original # Since these are auto generated, we can skip skip = ['secrets', 'imagePullSecrets'] if self.serviceaccount is None or \ not Utils.check_def_equal(self.prepared_router['ServiceAccount']['obj'].yaml_dict, self.serviceaccount.yaml_dict, skip_keys=skip, debug=self.verbose): self.prepared_router['ServiceAccount']['update'] = True # Secret: # See if one was generated from our dry-run and verify it if needed if self.prepared_router['Secret']['obj']: if not self.secret: self.prepared_router['Secret']['update'] = True if self.secret is None or \ not Utils.check_def_equal(self.prepared_router['Secret']['obj'].yaml_dict, self.secret.yaml_dict, skip_keys=skip, debug=self.verbose): self.prepared_router['Secret']['update'] = True # Service: # Fix the ports to have protocol=TCP for port in self.prepared_router['Service']['obj'].get('spec.ports'): port['protocol'] = 'TCP' skip = ['portalIP', 'clusterIP', 'sessionAffinity', 'type'] if self.service is None or \ not Utils.check_def_equal(self.prepared_router['Service']['obj'].yaml_dict, self.service.yaml_dict, skip_keys=skip, debug=self.verbose): self.prepared_router['Service']['update'] = True # DeploymentConfig: # Router needs some exceptions. # We do not want to check the autogenerated password for stats admin if self.deploymentconfig is not None: if not self.config.config_options['stats_password']['value']: for idx, env_var in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\ 'spec.template.spec.containers[0].env') or []): if env_var['name'] == 'STATS_PASSWORD': env_var['value'] = \ self.deploymentconfig.get('spec.template.spec.containers[0].env[%s].value' % idx) break # dry-run doesn't add the protocol to the ports section. We will manually do that. for idx, port in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\ 'spec.template.spec.containers[0].ports') or []): if not 'protocol' in port: port['protocol'] = 'TCP' # These are different when generating skip = ['dnsPolicy', 'terminationGracePeriodSeconds', 'restartPolicy', 'timeoutSeconds', 'livenessProbe', 'readinessProbe', 'terminationMessagePath', 'hostPort', 'defaultMode', ] if self.deploymentconfig is None or \ not Utils.check_def_equal(self.prepared_router['DeploymentConfig']['obj'].yaml_dict, self.deploymentconfig.yaml_dict, skip_keys=skip, debug=self.verbose): self.prepared_router['DeploymentConfig']['update'] = True # Check if any of the parts need updating, if so, return True # else, no need to update # pylint: disable=no-member return any([self.prepared_router[oc_type]['update'] for oc_type in self.prepared_router.keys()]) @staticmethod def run_ansible(params, check_mode): '''run ansible idempotent code''' rconfig = RouterConfig(params['name'], params['namespace'], params['kubeconfig'], {'default_cert': {'value': params['default_cert'], 'include': True}, 'cert_file': {'value': params['cert_file'], 'include': False}, 'key_file': {'value': params['key_file'], 'include': False}, 'images': {'value': params['images'], 'include': True}, 'latest_images': {'value': params['latest_images'], 'include': True}, 'labels': {'value': params['labels'], 'include': True}, 'ports': {'value': ','.join(params['ports']), 'include': True}, 'replicas': {'value': params['replicas'], 'include': True}, 'selector': {'value': params['selector'], 'include': True}, 'service_account': {'value': params['service_account'], 'include': True}, 'router_type': {'value': params['router_type'], 'include': False}, 'host_network': {'value': params['host_network'], 'include': True}, 'external_host': {'value': params['external_host'], 'include': True}, 'external_host_vserver': {'value': params['external_host_vserver'], 'include': True}, 'external_host_insecure': {'value': params['external_host_insecure'], 'include': True}, 'external_host_partition_path': {'value': params['external_host_partition_path'], 'include': True}, 'external_host_username': {'value': params['external_host_username'], 'include': True}, 'external_host_password': {'value': params['external_host_password'], 'include': True}, 'external_host_private_key': {'value': params['external_host_private_key'], 'include': True}, 'expose_metrics': {'value': params['expose_metrics'], 'include': True}, 'metrics_image': {'value': params['metrics_image'], 'include': True}, 'stats_user': {'value': params['stats_user'], 'include': True}, 'stats_password': {'value': params['stats_password'], 'include': True}, 'stats_port': {'value': params['stats_port'], 'include': True}, # extra 'cacert_file': {'value': params['cacert_file'], 'include': False}, # edits 'edits': {'value': params['edits'], 'include': False}, }) state = params['state'] ocrouter = Router(rconfig, verbose=params['debug']) api_rval = ocrouter.get() ######## # get ######## if state == 'list': return {'changed': False, 'results': api_rval, 'state': state} ######## # Delete ######## if state == 'absent': if not ocrouter.exists(): return {'changed': False, 'state': state} if check_mode: return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'} # In case of delete we return a list of each object # that represents a router and its result in a list # pylint: disable=redefined-variable-type api_rval = ocrouter.delete() return {'changed': True, 'results': api_rval, 'state': state} if state == 'present': ######## # Create ######## if not ocrouter.exists(): if check_mode: return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'} api_rval = ocrouter.create() if api_rval['returncode'] != 0: return {'failed': True, 'msg': api_rval} return {'changed': True, 'results': api_rval, 'state': state} ######## # Update ######## if not ocrouter.needs_update(): return {'changed': False, 'state': state} if check_mode: return {'changed': False, 'msg': 'CHECK_MODE: Would have performed an update.'} api_rval = ocrouter.update() if api_rval['returncode'] != 0: return {'failed': True, 'msg': api_rval} return {'changed': True, 'results': api_rval, 'state': state}