374 lines
13 KiB
Python
374 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
|
|
skip_delete:
|
|
description:
|
|
- Skip service/endpoint deletion or not
|
|
type: bool
|
|
default: true
|
|
'''
|
|
|
|
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)
|
|
self.real_domain_id = self.conn.session.auth.get_access(
|
|
self.conn.session).domain_id
|
|
|
|
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")
|
|
and current.domain_id != self.real_domain_id
|
|
):
|
|
conn = self.conn
|
|
else:
|
|
conn = self.conn.connect_as(target_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')
|
|
current_srv = existing_service_per_type.get(srv_type)
|
|
if (
|
|
current_srv
|
|
and (
|
|
current_srv.domain_id == self.real_domain_id
|
|
or not current_srv.domain_id
|
|
)
|
|
):
|
|
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()
|