358 lines
13 KiB
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 openstack import resource
from openstack.identity.v3 import service
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 Service(service.Service):
domain_id = resource.Body('domain_id')
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'),
skip_delete=dict(type='bool', default=True)
)
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):
if not hasattr(current, "domain_id") or not current.domain_id:
conn = self.conn
else:
conn = self.conn.connect_as(domain_id=current.domain_id)
_url = self.sdk.utils.urljoin(
self.identity_ext_base,
'OS-CATALOG', 'services', current.id
)
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._list(Service))
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,
'x_domain_id': srv.domain_id
})
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"],
skip_delete=self.params["skip_delete"]
)
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()