#!/usr/bin/python # # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' module: service_catalog extends_documentation_fragment: opentelekomcloud.backend.otc short_description: Manage Open Telekom Cloud service catalog version_added: "0.1.0" author: "Artem Goncharov (@gtema)" description: - Manages service catalog options: services: description: - Service Configuration data type: dict required: true environment: description: - Environment name type: str required: true limit_services: description: - optionally limit services type: list elements: str ''' RETURN = ''' ''' EXAMPLES = ''' ''' from urllib.parse import urlparse from ansible_collections.opentelekomcloud.backend.plugins.module_utils.otc import ( OTCModule ) def _is_srv_update_necessary(current, name, description, is_enabled): if current.name != name: return True if current.description != description: return True if current.is_enabled != is_enabled: return True return False def _is_ep_update_necessary(current, url, is_enabled=True): if current.url != url: return True if current.is_enabled != is_enabled: return True return False class ServiceCatalogModule(OTCModule): argument_spec = dict( services=dict(type='dict', required=True), environment=dict(type='str', required=True), limit_services=dict(type='list', elements='str'), ) module_kwargs = dict( supports_check_mode=True ) def _connect(self, cloud): identity_url = self.conn.identity.get_endpoint_data().url parsed_domain = urlparse(identity_url) self.identity_ext_base = '%s://%s/v3.0' % ( parsed_domain.scheme, parsed_domain.netloc) def _update_service(self, existing, name, description, is_enabled): _url = self.sdk.utils.urljoin( self.identity_ext_base, 'OS-CATALOG', 'services' ) return self.conn.identity._update( self.sdk.identity.v3.service.Service, existing, base_path=_url, name=name, description=description, is_enabled=is_enabled ) def _create_service(self, srv_type, name, description, is_enabled): _url = self.sdk.utils.urljoin(self.identity_ext_base, 'OS-CATALOG', 'services') return self.conn.identity._create( self.sdk.identity.v3.service.Service, base_path=_url, type=srv_type, name=name, description=description, is_enabled=is_enabled ) def _delete_service(self, current): _url = self.sdk.utils.urljoin( self.identity_ext_base, 'OS-CATALOG', 'services', current.id ) self.conn.identity.delete(_url) def _update_endpoint(self, current, url, is_enabled=True): _url = self.sdk.utils.urljoin( self.identity_ext_base, 'OS-CATALOG' ) return self.conn.identity._update( self.sdk.identity.v3.endpoint.Endpoint, current, base_path=_url, url=url, is_enabled=is_enabled ) def _create_endpoint( self, service_id, interface, url, region_id=None, is_enabled=True ): _url = self.sdk.utils.urljoin( self.identity_ext_base, 'OS-CATALOG', 'endpoints', ) return self.conn.identity._create( self.sdk.identity.v3.endpoint.Endpoint, base_path=_url, service_id=service_id, interface=interface, region_id=region_id, url=url, is_enabled=is_enabled ) def _delete_endpoint(self, current): _url = self.sdk.utils.urljoin( self.identity_ext_base, 'OS-CATALOG', current.id ) self.conn.identity.delete(_url) def process( self, services, target_env='production', dry_run=True, skip_delete=True, limit_services=None ): self._connect('dummy') changed = False existing_services = list(self.conn.identity.services()) existing_service_per_type = {k.type: k for k in existing_services} _used_services = set() _used_eps = set() results = dict(services=[], endpoints=[]) for srv_type, target_srv in services.items(): if limit_services is not None and srv_type not in limit_services: # logging.debug(f'Skipping service {srv_type} as requested') continue # logging.debug(f'Processing service {srv_type}') target_envs = target_srv.get('environments', {}) if not ( target_env in target_envs and target_envs.get(target_env) is not None ): # logging.debug(f'Skipping service {srv_type} for {target_env}') continue current_srv = None current_eps = [] target_name = target_srv.get('name') target_enabled = target_srv.get('enabled', True) target_description = target_srv.get('description') if srv_type and srv_type in existing_service_per_type: current_srv = existing_service_per_type.get(srv_type) if _is_srv_update_necessary( current_srv, target_name, target_description, target_enabled): changed = True if not dry_run: current_srv = self._update_service( current_srv, target_name, target_description, target_enabled) results['services'].append({ 'type': srv_type, 'operation': 'update', 'id': current_srv.id, 'current_name': current_srv.name, 'new_name': target_name, 'current_enabled': current_srv.is_enabled, 'new_enabled': target_enabled, 'current_description': current_srv.description, 'new_description': target_description, }) else: changed = True if not dry_run: current_srv = self._create_service( srv_type, target_name, target_description, target_enabled) results['services'].append({ 'type': srv_type, 'operation': 'create', 'new_name': target_name, 'new_enabled': target_enabled, 'new_description': target_description, }) if hasattr(current_srv, 'id'): # make dry_run easier _used_services.add(current_srv.id) current_eps = list(self.conn.identity.endpoints( service_id=current_srv.id)) target_ep = target_envs.get(target_env, {}) for region_id, target_eps in target_ep.get( 'endpoints', {}).items(): for tep in target_eps: # logging.debug(f'Checking ep {tep}') target_interface = tep.get('interface', 'public') target_url = tep['url'] # done explicitly to force # presence target_enabled = tep.get('enabled', True) _ep_found = False ep = None for cep in current_eps: # Dirty hack to compare region = None cep_region = cep.region_id if cep.region_id \ is not None else "" if ( region_id == cep_region and target_interface == cep.interface ): # Found matching EP _ep_found = True if _is_ep_update_necessary( cep, target_url, target_enabled ): changed = True if not dry_run: ep = self._update_endpoint( cep, target_url, is_enabled=target_enabled) else: ep = cep results['endpoints'].append({ 'service_type': srv_type, 'operation': 'update', 'id': cep.id, 'interface': target_interface, 'current_url': cep.url, 'new_url': target_url, 'current_enabled': cep.is_enabled, 'new_enabled': target_enabled, }) else: ep = cep break # no need to continue if not _ep_found: changed = True if not dry_run: ep = self._create_endpoint( current_srv.id, target_interface, target_url, region_id, target_enabled) results['endpoints'].append({ 'service_type': srv_type, 'operation': 'create', 'region_id': region_id, 'interface': target_interface, 'new_url': target_url, 'new_enabled': target_enabled, }) if hasattr(ep, 'id'): # Friendly dry_run _used_eps.add(ep.id) if not limit_services: # NOTE(gtema): Can't do cleanup of unused when filter requested # Cleanup useless endpoints for existing_ep in self.conn.identity.endpoints(): if existing_ep.id not in _used_eps: changed = True if not skip_delete and not dry_run: self._delete_endpoint(existing_ep) results['endpoints'].append({ 'operation': 'delete', 'id': existing_ep.id, 'current_url': existing_ep.url, 'current_enabled': existing_ep.is_enabled }) # Cleanup of unused or duplicated entries for srv in existing_services: if srv.id not in _used_services: changed = True if not skip_delete and not dry_run: self._delete_service(srv) results['services'].append({ 'type': srv.type, 'operation': 'delete', 'id': srv.id, 'current_name': srv.name, 'current_description': srv.description, 'current_enabled': srv.is_enabled }) return (results, changed) def run(self): changed = False (results, changed) = self.process( services=self.params['services'], target_env=self.params['environment'], dry_run=self.ansible.check_mode, limit_services=self.params['limit_services'] ) if len(self.errors) == 0: self.exit_json( changed=changed, changes=results ) else: self.fail_json( msg='Failures occured', errors=self.errors, ) def main(): module = ServiceCatalogModule() module() if __name__ == "__main__": main()