diff --git a/dbaas/tsuru/urls.py b/dbaas/tsuru/urls.py index 1d9fbedaa..0305659ba 100644 --- a/dbaas/tsuru/urls.py +++ b/dbaas/tsuru/urls.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- from django.conf.urls import url, patterns from views import (ListPlans, GetServiceStatus, GetServiceInfo, ServiceAdd, - ServiceAppBind, ServiceUnitBind, ServiceRemove) + ServiceAppBind, ServiceUnitBind, ServiceRemove, + ServiceJobBind) urlpatterns = patterns( 'tsuru.views', @@ -17,4 +18,6 @@ ServiceUnitBind.as_view()), url(r'^resources/(?P\w+)/bind-app$', ServiceAppBind.as_view(), name='service-app-bind'), + url(r'^resources/(?P\w+)/bind-job$', + ServiceJobBind.as_view(), name='service-job-bind'), ) diff --git a/dbaas/tsuru/views/__init__.py b/dbaas/tsuru/views/__init__.py index 88a05d16e..4df257a65 100644 --- a/dbaas/tsuru/views/__init__.py +++ b/dbaas/tsuru/views/__init__.py @@ -6,3 +6,4 @@ from serviceRemove import ServiceRemove from serviceUnitBind import ServiceUnitBind from serviceAppBind import ServiceAppBind +from serviceJobBind import ServiceJobBind diff --git a/dbaas/tsuru/views/serviceJobBind.py b/dbaas/tsuru/views/serviceJobBind.py new file mode 100644 index 000000000..028ddb8cd --- /dev/null +++ b/dbaas/tsuru/views/serviceJobBind.py @@ -0,0 +1,139 @@ +from rest_framework.views import APIView +from rest_framework.renderers import JSONRenderer, JSONPRenderer +from rest_framework.response import Response +from logical.models import Database +from workflow.steps.util.base import ACLFromHellClient +from ..utils import (log_and_response, get_url_env, LOG, check_database_status) +from rest_framework import status + + +class AclFromHellNotAllowerForEnvException(Exception): + pass + + +class ServiceJobBind(APIView): + renderer_classes = (JSONRenderer, JSONPRenderer) + model = Database + + def add_acl_for_hosts(self, database, job_name): + + infra = database.infra + hosts = infra.hosts + + acl_from_hell_client = ACLFromHellClient(database.environment) + if not acl_from_hell_client.aclfromhell_allowed: + raise AclFromHellNotAllowerForEnvException( + "ACL from hell credential not found for env {}".format( + database.environment + ) + ) + + for host in hosts: + acl_from_hell_client.add_job_acl(database, job_name, host.hostname) + acl_from_hell_client.add_job_acl_for_vip_if_needed(database, job_name) + + @staticmethod + def _handle_job_name(job_name): + return job_name[0] if isinstance(job_name, list) else job_name + + def post(self, request, database_name, format=None): + """This method binds a App to a database through tsuru.""" + env = get_url_env(request) + data = request.DATA + LOG.debug("Tsuru Bind App POST Request DATA {}".format(data)) + + response = check_database_status(database_name, env) + if not isinstance(response, self.model): + return response + + database = response + try: + self.add_acl_for_hosts( + database, + self._handle_job_name(data['job-name']) + ) + except Exception as e: + return log_and_response(str(e)) + + hosts, ports = database.infra.get_driver().get_dns_port() + ports = str(ports) + if database.databaseinfra.engine.name == 'redis': + redis_password = database.databaseinfra.password + endpoint = database.get_endpoint_dns().replace( + '', redis_password + ) + + env_vars = { + "DBAAS_REDIS_PASSWORD": redis_password, + "DBAAS_REDIS_ENDPOINT": endpoint, + "DBAAS_REDIS_HOST": hosts, + "DBAAS_REDIS_PORT": ports + } + + if 'redis_sentinel' in database.infra.get_driver().topology_name(): + env_vars = { + "DBAAS_SENTINEL_PASSWORD": redis_password, + "DBAAS_SENTINEL_ENDPOINT": endpoint, + "DBAAS_SENTINEL_ENDPOINT_SIMPLE": database.get_endpoint_dns_simple(), # noqa + "DBAAS_SENTINEL_SERVICE_NAME": database.databaseinfra.name, + "DBAAS_SENTINEL_HOSTS": hosts, + "DBAAS_SENTINEL_PORT": ports + } + + else: + try: + credential = ( + database.credentials.filter(privileges='Owner') + or database.credentials.all() + )[0] + except IndexError as e: + msg = ("Database {} in env {} does not have " + "credentials.").format( + database_name, env + ) + + return log_and_response( + msg=msg, e=e, + http_status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + endpoint = database.get_endpoint_dns().replace( + ':', "{}:{}".format( + credential.user, credential.password + ) + ) + + kind = '' + if endpoint.startswith('mysql'): + kind = 'MYSQL_' + if endpoint.startswith('mongodb'): + kind = 'MONGODB_' + + env_vars = { + "DBAAS_{}USER".format(kind): credential.user, + "DBAAS_{}PASSWORD".format(kind): credential.password, + "DBAAS_{}ENDPOINT".format(kind): endpoint, + "DBAAS_{}HOSTS".format(kind): hosts, + "DBAAS_{}PORT".format(kind): ports + } + + return Response(env_vars, status.HTTP_201_CREATED) + + def delete(self, request, database_name, format=None): + """This method unbinds a Job to a database through tsuru.""" + env = get_url_env(request) + data = request.DATA + LOG.debug("Tsuru Unbind Job DELETE Request DATA {}".format(data)) + + response = check_database_status(database_name, env) + if not isinstance(response, Database): + return response + + database = response + + acl_from_hell_client = ACLFromHellClient(database.environment) + acl_from_hell_client.remove_job_acl( + database, + self._handle_job_name(data['job-name']) + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/dbaas/workflow/steps/util/base.py b/dbaas/workflow/steps/util/base.py index 50533a6c5..45f214186 100644 --- a/dbaas/workflow/steps/util/base.py +++ b/dbaas/workflow/steps/util/base.py @@ -596,6 +596,14 @@ def add_acl(self, database, app_name, hostname): hostname, resp.status_code )) + def add_job_acl_for_vip_if_needed(self, database, job_name): + databaseinfra = database.databaseinfra + if not databaseinfra.vips.exists(): + return + + vip_dns = self._get_vip_dns(databaseinfra) + self.add_job_acl(database, job_name, vip_dns) + def remove_acl(self, database, app_name): rules = self.get_enabled_rules(database, app_name) @@ -621,3 +629,124 @@ def remove_acl(self, database, app_name): rule_id, host) LOG.error(msg) return None + + def add_job_acl(self, database, job_name, hostname): + rules = self.get_job_enabled_rules( + database, + job_name, + extra_params={'destination.externaldns.name': hostname} + ) + if rules: + msg = "Rule already registered. Database: {}, \ + Job Name: {}, Hostname: {} - Rules: {}".format( + database, job_name, hostname, rules + ) + LOG.info(msg) + return + + infra = database.infra + driver = infra.get_driver() + + payload = { + "source": { + "tsurujob": { + "jobname": job_name + } + }, + "destination": { + "externaldns": { + "name": hostname, + "ports": map( + lambda p: { + 'protocol': 'tcp', + 'port': p + }, + driver.ports + ) + } + }, + "target": "accept", + "metadata": { + 'owner': 'dbaas', + "service-name": self.credential.project, + "instance-name": database.name + } + } + + LOG.info("Tsuru Add ACL For Job: payload for host {}:{}".format( + hostname, payload)) + resp = self._request( + requests.post, + self.credential.endpoint, + json=payload, + ) + if not resp.ok: + error = "Cant set acl for job {}:{}-{}. Error: {}".format( + job_name, database, hostname, resp.content + ) + LOG.error(msg) + raise CantSetACLError(error) + + LOG.info("Tsuru Add ACL Status for host {}: {}".format( + hostname, resp.status_code + )) + + def remove_job_acl(self, database, job_name): + rules = self.get_job_enabled_rules(database, job_name) + + if not rules: + msg = "Rule not found for {}.".format( + database.name) + LOG.debug(msg) + + for rule in rules: + rule_id = rule.get('RuleID') + host = (rule.get('Destination', {}) + .get('ExternalDNS', {}) + .get('Name')) + if rule_id: + LOG.info('Tsuru Unbind App removing rule for job{}:{}-{}'.format( + job_name, database, host)) + resp = self._request( + requests.delete, + '{}/{}'.format(self.credential.endpoint, rule_id) + ) + if not resp.ok: + msg = "Error on delete rule {} for {}.".format( + rule_id, host) + LOG.error(msg) + return None + + def get_job_enabled_rules(self, database, job_name, extra_params=None): + enabled_rules = [] + params = { + 'metadata.owner': 'dbaas', + 'metadata.service-name': self.credential.project, + 'metadata.instance-name': database.name, + 'source.tsurujob.jobname': job_name, + } + + if extra_params: + params.update(extra_params) + + LOG.debug("Tsuru get rule for {} params:{}".format( + database.name, params)) + resp = self._request( + requests.get, + self.credential.endpoint, + params=params, + ) + + if not resp.ok: + LOG.debug("Tsuru Status on Get Rules for database {}: {}".format( + database, resp.status_code)) + return enabled_rules + + all_rules = resp.json() + if all_rules: + for rule in all_rules: + if rule.get('Removed'): + continue + enabled_rules.append(rule) + + return enabled_rules