289 lines
10 KiB
Python
289 lines
10 KiB
Python
# Copyright (c) 2015 Mirantis, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import base64
|
|
import json
|
|
import uuid
|
|
|
|
import muranoclient.client as client
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
import six
|
|
from webob import exc
|
|
|
|
from murano.api.v1.cloudfoundry import auth as keystone_auth
|
|
from murano.common.i18n import _LI, _LW
|
|
from murano.common import wsgi
|
|
from murano import context
|
|
from murano.db.catalog import api as db_api
|
|
from murano.db.services import cf_connections as db_cf
|
|
|
|
|
|
cfapi_opts = [
|
|
cfg.StrOpt('tenant', default='admin',
|
|
help=('Tenant for service broker')),
|
|
cfg.StrOpt('bind_host', default='localhost',
|
|
help=('host for service broker')),
|
|
cfg.StrOpt('bind_port', default='8083',
|
|
help=('host for service broker')),
|
|
cfg.StrOpt('auth_url', default='localhost:5000/v2.0')]
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(cfapi_opts, group='cfapi')
|
|
|
|
|
|
class Controller(object):
|
|
"""WSGI controller for application catalog resource in Murano v1 API"""
|
|
|
|
def _package_to_service(self, package):
|
|
srv = {}
|
|
srv['id'] = package.id
|
|
srv['name'] = package.name
|
|
srv['description'] = package.description
|
|
srv['bindable'] = True
|
|
srv['tags'] = []
|
|
for tag in package.tags:
|
|
srv['tags'].append(tag.name)
|
|
plan = {'id': package.id + '-1',
|
|
'name': 'default',
|
|
'description': 'Default plan for the service {name}'.format(
|
|
name=package.name)}
|
|
srv['plans'] = [plan]
|
|
return srv
|
|
|
|
def _check_auth(self, req, tenant=None):
|
|
auth = req.headers.get('Authorization', None)
|
|
if auth is None:
|
|
raise exc.HTTPUnauthorized(explanation='Bad credentials')
|
|
|
|
auth_info = auth.split(' ')[1]
|
|
auth_decoded = base64.b64decode(auth_info)
|
|
user = auth_decoded.split(':')[0]
|
|
password = auth_decoded.split(':')[1]
|
|
if tenant:
|
|
keystone = keystone_auth.authenticate(user, password, tenant)
|
|
else:
|
|
keystone = keystone_auth.authenticate(user, password)
|
|
return (user, password, keystone)
|
|
|
|
def _make_service(self, name, package, plan_id):
|
|
id = uuid.uuid4().hex
|
|
|
|
return {"name": name,
|
|
"?": {plan_id: {"name": package.name},
|
|
"type": package.fully_qualified_name,
|
|
"id": id}}
|
|
|
|
def _get_service(self, env, service_id):
|
|
for service in env.services:
|
|
if service['?']['id'] == service_id:
|
|
return service
|
|
return None
|
|
|
|
def list(self, req):
|
|
user, _, keystone = self._check_auth(req)
|
|
# Once we get here we were authorized by keystone
|
|
token = keystone.auth_token
|
|
|
|
ctx = context.RequestContext(user=user, tenant='', auth_token=token)
|
|
|
|
packages = db_api.package_search({'type': 'application'}, ctx,
|
|
catalog=True)
|
|
services = []
|
|
for package in packages:
|
|
services.append(self._package_to_service(package))
|
|
|
|
resp = {'services': services}
|
|
|
|
return resp
|
|
|
|
def provision(self, req, body, instance_id):
|
|
"""Here is the example of request body given us from Cloud Foundry:
|
|
|
|
{
|
|
"service_id": "service-guid-here",
|
|
"plan_id": "plan-guid-here",
|
|
"organization_guid": "org-guid-here",
|
|
"space_guid": "space-guid-here",
|
|
"parameters": {"param1": "value1",
|
|
"param2": "value2"}
|
|
}
|
|
"""
|
|
data = json.loads(req.body)
|
|
space_guid = data['space_guid']
|
|
org_guid = data['organization_guid']
|
|
plan_id = data['plan_id']
|
|
service_id = data['service_id']
|
|
parameters = data['parameters']
|
|
self.current_session = None
|
|
|
|
# Here we'll take an entry for CF org and space from db. If we
|
|
# don't have any entries we will create it from scratch.
|
|
try:
|
|
tenant = db_cf.get_tenant_for_org(org_guid)
|
|
except AttributeError:
|
|
# FIXME(Kezar): need to find better way to get tenant
|
|
tenant = CONF.cfapi.tenant
|
|
db_cf.set_tenant_for_org(org_guid, tenant)
|
|
LOG.info(_LI("Cloud Foundry {org_id} mapped to tenant "
|
|
"{tenant_name}").format(org_id=org_guid,
|
|
tenant_name=tenant))
|
|
|
|
# Now as we have all parameters we can try to auth user in actual
|
|
# tenant
|
|
|
|
user, _, keystone = self._check_auth(req, tenant)
|
|
# Once we get here we were authorized by keystone
|
|
token = keystone.auth_token
|
|
m_cli = muranoclient(token)
|
|
try:
|
|
environment_id = db_cf.get_environment_for_space(space_guid)
|
|
except AttributeError:
|
|
body = {'name': 'my_{uuid}'.format(uuid=uuid.uuid4().hex)}
|
|
env = m_cli.environments.create(body)
|
|
environment_id = env.id
|
|
db_cf.set_environment_for_space(space_guid, environment_id)
|
|
LOG.info(_LI("Cloud Foundry {space_id} mapped to {environment_id}")
|
|
.format(space_id=space_guid,
|
|
environment_id=environment_id))
|
|
|
|
LOG.debug('Keystone endpoint: {0}'.format(keystone.auth_ref))
|
|
tenant_id = keystone.project_id
|
|
ctx = context.RequestContext(user=user, tenant=tenant_id)
|
|
|
|
package = db_api.package_get(service_id, ctx)
|
|
LOG.debug('Adding service {name}'.format(name=package.name))
|
|
|
|
service = self._make_service(space_guid, package, plan_id)
|
|
db_cf.set_instance_for_service(instance_id, service['?']['id'],
|
|
environment_id, tenant)
|
|
# NOTE(Kezar): Here we are going through JSON and add ids where
|
|
# it's necessary
|
|
params = [parameters]
|
|
while params:
|
|
a = params.pop()
|
|
for k, v in a.iteritems():
|
|
if isinstance(v, dict):
|
|
params.append(v)
|
|
if k == '?':
|
|
v['id'] = uuid.uuid4().hex
|
|
service.update(parameters)
|
|
# Now we need to obtain session to modify the env
|
|
session_id = create_session(m_cli, environment_id)
|
|
m_cli.services.post(environment_id,
|
|
path='/',
|
|
data=service,
|
|
session_id=session_id)
|
|
m_cli.sessions.deploy(environment_id, session_id)
|
|
self.current_session = session_id
|
|
return {}
|
|
|
|
def deprovision(self, req, instance_id):
|
|
service = db_cf.get_service_for_instance(instance_id)
|
|
if not service:
|
|
return {}
|
|
|
|
service_id = service.service_id
|
|
environment_id = service.environment_id
|
|
tenant = service.tenant
|
|
_, _, keystone = self._check_auth(req, tenant)
|
|
# Once we get here we were authorized by keystone
|
|
token = keystone.auth_token
|
|
m_cli = muranoclient(token)
|
|
|
|
try:
|
|
session_id = create_session(m_cli, environment_id)
|
|
except exc.HTTPForbidden:
|
|
# FIXME(Kezar): this is a temporary solution, should be replaced
|
|
# with 'incomplete' response for Cloud Foudry as soon as we will
|
|
# know which is right format for it.
|
|
LOG.warning(_LW("Can't create new session. Please remove service "
|
|
"manually in environment {0}")
|
|
.format(environment_id))
|
|
return {}
|
|
|
|
m_cli.services.delete(environment_id, '/' + service_id, session_id)
|
|
m_cli.sessions.deploy(environment_id, session_id)
|
|
return {}
|
|
|
|
def bind(self, req, body, instance_id, app_id):
|
|
filtered = [u'?', u'instance']
|
|
db_service = db_cf.get_service_for_instance(instance_id)
|
|
if not db_service:
|
|
return {}
|
|
|
|
service_id = db_service.service_id
|
|
environment_id = db_service.environment_id
|
|
tenant = db_service.tenant
|
|
_, _, keystone = self._check_auth(req, tenant)
|
|
# Once we get here we were authorized by keystone
|
|
token = keystone.auth_token
|
|
m_cli = muranoclient(token)
|
|
|
|
session_id = create_session(m_cli, environment_id)
|
|
env = m_cli.environments.get(environment_id, session_id)
|
|
LOG.debug('Got environment {0}'.format(env))
|
|
service = self._get_service(env, service_id)
|
|
LOG.debug('Got service {0}'.format(service))
|
|
credentials = {}
|
|
for k, v in six.iteritems(service):
|
|
if k not in filtered:
|
|
credentials[k] = v
|
|
|
|
return {'credentials': credentials}
|
|
|
|
def unbind(self, req, instance_id, app_id):
|
|
"""Unsupported functionality
|
|
|
|
murano doesn't support this kind of functionality, so we just need
|
|
to create a stub where the call will come. We can't raise something
|
|
like NotImplementedError because we will have problems on Cloud Foundry
|
|
side. The best way now it to return empty dict which will be correct
|
|
answer for Cloud Foundry.
|
|
"""
|
|
|
|
return {}
|
|
|
|
def get_last_operation(self):
|
|
"""Not implemented functionality
|
|
|
|
For some reason it's difficult to provide a valid JSON with the
|
|
response code which is needed for our broker to be true asynchronous.
|
|
In that case last_operation API call is not supported.
|
|
"""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
def muranoclient(token_id):
|
|
endpoint = "http://{murano_host}:{murano_port}".format(
|
|
murano_host=CONF.bind_host, murano_port=CONF.bind_port)
|
|
insecure = False
|
|
|
|
LOG.debug('murano client created. Murano::Client <Url: {endpoint}'.format(
|
|
endpoint=endpoint))
|
|
|
|
return client.Client(1, endpoint=endpoint, token=token_id,
|
|
insecure=insecure)
|
|
|
|
|
|
def create_session(client, environment_id):
|
|
id = client.sessions.configure(environment_id).id
|
|
return id
|
|
|
|
|
|
def create_resource():
|
|
return wsgi.Resource(Controller())
|