Merge "Cloud Foundry Service Broker API initial commit"
This commit is contained in:
commit
46b6e14428
@ -1,3 +1,6 @@
|
|||||||
|
[pipeline:cloudfoundry]
|
||||||
|
pipeline = cloudfoundryapi
|
||||||
|
|
||||||
[pipeline:murano]
|
[pipeline:murano]
|
||||||
pipeline = request_id versionnegotiation faultwrap authtoken context rootapp
|
pipeline = request_id versionnegotiation faultwrap authtoken context rootapp
|
||||||
|
|
||||||
@ -20,6 +23,9 @@ paste.app_factory = murano.api.versions:create_resource
|
|||||||
[app:apiv1app]
|
[app:apiv1app]
|
||||||
paste.app_factory = murano.api.v1.router:API.factory
|
paste.app_factory = murano.api.v1.router:API.factory
|
||||||
|
|
||||||
|
[app:cloudfoundryapi]
|
||||||
|
paste.app_factory = murano.api.v1.cloudfoundry.router:API.factory
|
||||||
|
|
||||||
[filter:versionnegotiation]
|
[filter:versionnegotiation]
|
||||||
paste.filter_factory = murano.api.middleware.version_negotiation:VersionNegotiationFilter.factory
|
paste.filter_factory = murano.api.middleware.version_negotiation:VersionNegotiationFilter.factory
|
||||||
|
|
||||||
|
0
murano/api/v1/cloudfoundry/__init__.py
Normal file
0
murano/api/v1/cloudfoundry/__init__.py
Normal file
30
murano/api/v1/cloudfoundry/auth.py
Normal file
30
murano/api/v1/cloudfoundry/auth.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from keystoneclient.v3 import client
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate(user, password, tenant=None):
|
||||||
|
project_name = tenant or CONF.cfapi.tenant
|
||||||
|
keystone = client.Client(username=user,
|
||||||
|
password=password,
|
||||||
|
project_name=project_name,
|
||||||
|
auth_url=CONF.cfapi.auth_url.replace(
|
||||||
|
'v2.0', 'v3'))
|
||||||
|
return keystone
|
249
murano/api/v1/cloudfoundry/cfapi.py
Normal file
249
murano/api/v1/cloudfoundry/cfapi.py
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
# 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
|
||||||
|
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 list(self, req):
|
||||||
|
user, passwd, 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, passwd, 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('Auth: %s' % 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
|
||||||
|
user, passwd, 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, instance_id, id):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def unbind(self, req, instance_id, id):
|
||||||
|
pass
|
||||||
|
|
||||||
|
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())
|
45
murano/api/v1/cloudfoundry/router.py
Normal file
45
murano/api/v1/cloudfoundry/router.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# 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 routes
|
||||||
|
|
||||||
|
from murano.api.v1.cloudfoundry import cfapi
|
||||||
|
|
||||||
|
from murano.common import wsgi
|
||||||
|
|
||||||
|
|
||||||
|
class API(wsgi.Router):
|
||||||
|
@classmethod
|
||||||
|
def factory(cls, global_conf, **local_conf):
|
||||||
|
return cls(routes.Mapper())
|
||||||
|
|
||||||
|
def __init__(self, mapper):
|
||||||
|
services_resource = cfapi.create_resource()
|
||||||
|
mapper.connect('/v2/catalog',
|
||||||
|
controller=services_resource,
|
||||||
|
action='list',
|
||||||
|
conditions={'method': ['GET']})
|
||||||
|
mapper.connect(('/v2/service_instances/{instance_id}'),
|
||||||
|
controller=services_resource,
|
||||||
|
action='provision',
|
||||||
|
conditions={'method': ['PUT']})
|
||||||
|
mapper.connect(('/v2/service_instances/{instance_id}'),
|
||||||
|
controller=services_resource,
|
||||||
|
action='deprovision',
|
||||||
|
conditions={'method': ['DELETE']})
|
||||||
|
mapper.connect(('/v2/service_instances/{instance_id}/last_operation'),
|
||||||
|
controller=services_resource,
|
||||||
|
action='get_last_operation',
|
||||||
|
conditions={'method': ['GET']})
|
||||||
|
|
||||||
|
super(API, self).__init__(mapper)
|
75
murano/cmd/cfapi.py
Normal file
75
murano/cmd/cfapi.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env 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 os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import eventlet
|
||||||
|
|
||||||
|
if os.name == 'nt':
|
||||||
|
# eventlet monkey patching causes subprocess.Popen to fail on Windows
|
||||||
|
# when using pipes due to missing non blocking I/O support
|
||||||
|
eventlet.monkey_patch(os=False)
|
||||||
|
else:
|
||||||
|
eventlet.monkey_patch()
|
||||||
|
|
||||||
|
# If ../murano/__init__.py exists, add ../ to Python search path, so that
|
||||||
|
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||||
|
root = os.path.join(os.path.abspath(__file__), os.pardir, os.pardir, os.pardir)
|
||||||
|
if os.path.exists(os.path.join(root, 'murano', '__init__.py')):
|
||||||
|
sys.path.insert(0, root)
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_service import service
|
||||||
|
|
||||||
|
from murano.api.v1 import request_statistics
|
||||||
|
from murano.common import app_loader
|
||||||
|
from murano.common import config
|
||||||
|
from murano.common import policy
|
||||||
|
from murano.common import server
|
||||||
|
from murano.common import wsgi
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
config.parse_args()
|
||||||
|
logging.setup(CONF, 'murano-cfapi')
|
||||||
|
request_statistics.init_stats()
|
||||||
|
policy.init()
|
||||||
|
|
||||||
|
launcher = service.ServiceLauncher(CONF)
|
||||||
|
|
||||||
|
cfapp = app_loader.load_paste_app('cloudfoundry')
|
||||||
|
cfport, cfhost = (config.CONF.cfapi.bind_port,
|
||||||
|
config.CONF.cfapi.bind_host)
|
||||||
|
|
||||||
|
launcher.launch_service(wsgi.Service(cfapp, cfport, cfhost))
|
||||||
|
|
||||||
|
launcher.launch_service(server.get_rpc_service())
|
||||||
|
launcher.launch_service(server.get_notification_service())
|
||||||
|
|
||||||
|
launcher.wait()
|
||||||
|
except RuntimeError as e:
|
||||||
|
sys.stderr.write("ERROR: %s\n" % e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,68 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Revision ID: 009
|
||||||
|
Revises: None
|
||||||
|
Create Date: 2015-08-17 16:34:33.698760
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '009'
|
||||||
|
down_revision = '008'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
MYSQL_ENGINE = 'InnoDB'
|
||||||
|
MYSQL_CHARSET = 'utf8'
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table(
|
||||||
|
'cf_orgs',
|
||||||
|
sa.Column('id', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('tenant', sa.String(length=255), nullable=False),
|
||||||
|
sa.UniqueConstraint('tenant'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
mysql_engine=MYSQL_ENGINE,
|
||||||
|
mysql_charset=MYSQL_CHARSET)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'cf_spaces',
|
||||||
|
sa.Column('id', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('environment_id', sa.String(length=255), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['environment_id'], ['environment.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
mysql_engine=MYSQL_ENGINE,
|
||||||
|
mysql_charset=MYSQL_CHARSET)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'cf_serv_inst',
|
||||||
|
sa.Column('id', sa.String(length=255), primary_key=True),
|
||||||
|
sa.Column('service_id', sa.String(255), nullable=False),
|
||||||
|
sa.Column('environment_id', sa.String(255), nullable=False),
|
||||||
|
sa.Column('tenant', sa.String(255), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['environment_id'], ['environment.id'],),
|
||||||
|
mysql_engine=MYSQL_ENGINE,
|
||||||
|
mysql_charset=MYSQL_CHARSET)
|
||||||
|
# end Alembic commands #
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table('cf_orgs')
|
||||||
|
op.drop_table('cf_spaces')
|
||||||
|
op.drop_table('cf_serv_inst')
|
||||||
|
# end Alembic commands #
|
@ -74,6 +74,13 @@ class Environment(Base, TimestampMixin):
|
|||||||
tasks = sa_orm.relationship('Task', backref='environment',
|
tasks = sa_orm.relationship('Task', backref='environment',
|
||||||
cascade='save-update, merge, delete')
|
cascade='save-update, merge, delete')
|
||||||
|
|
||||||
|
cf_spaces = sa_orm.relationship("CFSpace", backref='environment',
|
||||||
|
cascade='save-update, merge, delete')
|
||||||
|
|
||||||
|
cf_serv_inst = sa_orm.relationship("CFServiceInstance",
|
||||||
|
backref='environment',
|
||||||
|
cascade='save-update, merge, delete')
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
dictionary = super(Environment, self).to_dict()
|
dictionary = super(Environment, self).to_dict()
|
||||||
del dictionary['description']
|
del dictionary['description']
|
||||||
@ -320,10 +327,45 @@ class Lock(Base):
|
|||||||
ts = sa.Column(sa.DateTime, nullable=False)
|
ts = sa.Column(sa.DateTime, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class CFOrganization(Base):
|
||||||
|
__tablename__ = "cf_orgs"
|
||||||
|
id = sa.Column(sa.String(255), primary_key=True)
|
||||||
|
tenant = sa.Column(sa.String(255), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class CFSpace(Base):
|
||||||
|
__tablename__ = "cf_spaces"
|
||||||
|
id = sa.Column(sa.String(255), primary_key=True)
|
||||||
|
environment_id = sa.Column(sa.String(255), sa.ForeignKey('environment.id'),
|
||||||
|
nullable=False)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
dictionary = super(CFSpace, self).to_dict()
|
||||||
|
if 'environment' in dictionary:
|
||||||
|
del dictionary['environment']
|
||||||
|
return dictionary
|
||||||
|
|
||||||
|
|
||||||
|
class CFServiceInstance(Base):
|
||||||
|
__tablename__ = 'cf_serv_inst'
|
||||||
|
id = sa.Column(sa.String(255), primary_key=True)
|
||||||
|
service_id = sa.Column(sa.String(255), nullable=False)
|
||||||
|
environment_id = sa.Column(sa.String(255), sa.ForeignKey('environment.id'),
|
||||||
|
nullable=False)
|
||||||
|
tenant = sa.Column(sa.String(255), nullable=False)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
dictionary = super(CFSpace, self).to_dict()
|
||||||
|
if 'environment' in dictionary:
|
||||||
|
del dictionary['environment']
|
||||||
|
return dictionary
|
||||||
|
|
||||||
|
|
||||||
def register_models(engine):
|
def register_models(engine):
|
||||||
"""Creates database tables for all models with the given engine."""
|
"""Creates database tables for all models with the given engine."""
|
||||||
models = (Environment, Status, Session, Task,
|
models = (Environment, Status, Session, Task,
|
||||||
ApiStats, Package, Category, Class, Instance, Lock)
|
ApiStats, Package, Category, Class, Instance, Lock, CFSpace,
|
||||||
|
CFOrganization)
|
||||||
for model in models:
|
for model in models:
|
||||||
model.metadata.create_all(engine)
|
model.metadata.create_all(engine)
|
||||||
|
|
||||||
@ -331,6 +373,7 @@ def register_models(engine):
|
|||||||
def unregister_models(engine):
|
def unregister_models(engine):
|
||||||
"""Drops database tables for all models with the given engine."""
|
"""Drops database tables for all models with the given engine."""
|
||||||
models = (Environment, Status, Session, Task,
|
models = (Environment, Status, Session, Task,
|
||||||
ApiStats, Package, Category, Class, Lock)
|
ApiStats, Package, Category, Class, Lock, CFOrganization,
|
||||||
|
CFSpace)
|
||||||
for model in models:
|
for model in models:
|
||||||
model.metadata.drop_all(engine)
|
model.metadata.drop_all(engine)
|
||||||
|
91
murano/db/services/cf_connections.py
Normal file
91
murano/db/services/cf_connections.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_db import exception
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
from murano.db import models
|
||||||
|
from murano.db import session as db_session
|
||||||
|
|
||||||
|
|
||||||
|
def set_tenant_for_org(cf_org_id, tenant):
|
||||||
|
"""Store tenant-org link to db"""
|
||||||
|
unit = db_session.get_session()
|
||||||
|
try:
|
||||||
|
with unit.begin():
|
||||||
|
org = models.CFOrganization()
|
||||||
|
org.id = cf_org_id
|
||||||
|
org.tenant = tenant
|
||||||
|
unit.add(org)
|
||||||
|
except exception.DBDuplicateEntry:
|
||||||
|
unit.execute(sqlalchemy.update(models.CFOrganization).where(
|
||||||
|
models.CFOrganization.id == cf_org_id).values(
|
||||||
|
tenant=tenant))
|
||||||
|
|
||||||
|
|
||||||
|
def set_environment_for_space(cf_space_id, environment_id):
|
||||||
|
"""Store env-space link to db"""
|
||||||
|
unit = db_session.get_session()
|
||||||
|
try:
|
||||||
|
with unit.begin():
|
||||||
|
space = models.CFSpace()
|
||||||
|
space.id = cf_space_id
|
||||||
|
space.environment_id = environment_id
|
||||||
|
unit.add(space)
|
||||||
|
except exception.DBDuplicateEntry:
|
||||||
|
unit.execute(sqlalchemy.update(models.CFSpace).where(
|
||||||
|
models.CFSpace.id == cf_space_id).values(
|
||||||
|
environment_id=environment_id))
|
||||||
|
|
||||||
|
|
||||||
|
def set_instance_for_service(instance_id, service_id, environment_id, tenant):
|
||||||
|
"""Store env-space link to db"""
|
||||||
|
unit = db_session.get_session()
|
||||||
|
try:
|
||||||
|
with unit.begin():
|
||||||
|
connection = models.CFServiceInstance()
|
||||||
|
connection.id = instance_id
|
||||||
|
connection.service_id = service_id
|
||||||
|
connection.environment_id = environment_id
|
||||||
|
connection.tenant = tenant
|
||||||
|
unit.add(connection)
|
||||||
|
except exception.DBDuplicateEntry:
|
||||||
|
unit.execute(sqlalchemy.update(models.CFServiceInstance).where(
|
||||||
|
models.CFServiceInstance.id == instance_id).values(
|
||||||
|
environment_id=environment_id))
|
||||||
|
|
||||||
|
|
||||||
|
def get_environment_for_space(cf_space_id):
|
||||||
|
"""Take env id related to space from db"""
|
||||||
|
unit = db_session.get_session()
|
||||||
|
connection = unit.query(models.CFSpace).get(cf_space_id)
|
||||||
|
return connection.environment_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_tenant_for_org(cf_org_id):
|
||||||
|
"""Take tenant id related to org from db"""
|
||||||
|
unit = db_session.get_session()
|
||||||
|
connection = unit.query(models.CFOrganization).get(cf_org_id)
|
||||||
|
return connection.tenant
|
||||||
|
|
||||||
|
|
||||||
|
def get_service_for_instance(instance_id):
|
||||||
|
unit = db_session.get_session()
|
||||||
|
connection = unit.query(models.CFServiceInstance).get(instance_id)
|
||||||
|
return connection
|
||||||
|
|
||||||
|
|
||||||
|
def delete_environment_from_space(environment_id):
|
||||||
|
unit = db_session.get_session()
|
||||||
|
unit.query(models.CFSpace).filter(
|
||||||
|
models.CFSpace.environment_id == environment_id).delete(
|
||||||
|
synchronize=False)
|
0
murano/tests/unit/api/v1/cloudfoundry/__init__.py
Normal file
0
murano/tests/unit/api/v1/cloudfoundry/__init__.py
Normal file
129
murano/tests/unit/api/v1/cloudfoundry/test_cfapi.py
Normal file
129
murano/tests/unit/api/v1/cloudfoundry/test_cfapi.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# 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 mock
|
||||||
|
|
||||||
|
from murano.api.v1.cloudfoundry import cfapi as api
|
||||||
|
from murano.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestController(base.MuranoTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestController, self).setUp()
|
||||||
|
self.controller = api.Controller()
|
||||||
|
|
||||||
|
self.request = mock.MagicMock()
|
||||||
|
self.request.headers = {'Authorization': 'Basic {encoded}'.format(
|
||||||
|
encoded=base64.b64encode('test:test'))}
|
||||||
|
|
||||||
|
@mock.patch('murano.common.policy.check_is_admin')
|
||||||
|
@mock.patch('murano.db.catalog.api.package_search')
|
||||||
|
@mock.patch('murano.api.v1.cloudfoundry.auth.authenticate')
|
||||||
|
def test_list(self, mock_auth, mock_db_search, mock_policy):
|
||||||
|
|
||||||
|
pkg0 = mock.MagicMock()
|
||||||
|
pkg0.id = 'xxx'
|
||||||
|
pkg0.name = 'foo'
|
||||||
|
pkg0.description = 'stub pkg'
|
||||||
|
|
||||||
|
mock_db_search.return_value = [pkg0]
|
||||||
|
|
||||||
|
answer = {'services': [{'bindable': True,
|
||||||
|
'description': pkg0.description,
|
||||||
|
'id': pkg0.id,
|
||||||
|
'name': pkg0.name,
|
||||||
|
'plans': [{'description': ('Default plan for '
|
||||||
|
'the service '
|
||||||
|
'{name}').format(
|
||||||
|
name=pkg0.name),
|
||||||
|
'id': 'xxx-1',
|
||||||
|
'name': 'default'}],
|
||||||
|
'tags': []}]}
|
||||||
|
|
||||||
|
resp = self.controller.list(self.request)
|
||||||
|
self.assertEqual(answer, resp)
|
||||||
|
|
||||||
|
@mock.patch('murano.common.policy.check_is_admin')
|
||||||
|
@mock.patch('murano.db.catalog.api.package_get')
|
||||||
|
@mock.patch('murano.api.v1.cloudfoundry.cfapi.muranoclient')
|
||||||
|
@mock.patch('murano.db.services.cf_connections.set_instance_for_service')
|
||||||
|
@mock.patch('murano.db.services.cf_connections.get_environment_for_space')
|
||||||
|
@mock.patch('murano.db.services.cf_connections.get_tenant_for_org')
|
||||||
|
@mock.patch('murano.api.v1.cloudfoundry.auth.authenticate')
|
||||||
|
def test_provision_from_scratch(self, mock_auth, mock_get_tenant,
|
||||||
|
mock_get_environment, mock_is, mock_client,
|
||||||
|
mock_package, mock_policy):
|
||||||
|
|
||||||
|
body = {"space_guid": "s1-p1",
|
||||||
|
"organization_guid": "o1-r1",
|
||||||
|
"plan_id": "p1-l1",
|
||||||
|
"service_id": "s1-e1",
|
||||||
|
"parameters": {'some_parameter': 'value',
|
||||||
|
'?': {}}}
|
||||||
|
self.request.body = json.dumps(body)
|
||||||
|
|
||||||
|
mock_get_environment.return_value = '555-555'
|
||||||
|
mock_client.return_value = mock.MagicMock()
|
||||||
|
mock_package.return_value = mock.MagicMock()
|
||||||
|
|
||||||
|
resp = self.controller.provision(self.request, {}, '111-111')
|
||||||
|
|
||||||
|
self.assertEqual({}, resp)
|
||||||
|
|
||||||
|
@mock.patch('murano.common.policy.check_is_admin')
|
||||||
|
@mock.patch('murano.db.catalog.api.package_get')
|
||||||
|
@mock.patch('murano.api.v1.cloudfoundry.cfapi.muranoclient')
|
||||||
|
@mock.patch('murano.db.services.cf_connections.set_instance_for_service')
|
||||||
|
@mock.patch('murano.db.services.cf_connections.set_environment_for_space')
|
||||||
|
@mock.patch('murano.db.services.cf_connections.set_tenant_for_org')
|
||||||
|
@mock.patch('murano.db.services.cf_connections.get_environment_for_space')
|
||||||
|
@mock.patch('murano.db.services.cf_connections.get_tenant_for_org')
|
||||||
|
@mock.patch('murano.api.v1.cloudfoundry.auth.authenticate')
|
||||||
|
def test_provision_existent(self, mock_auth, mock_get_tenant,
|
||||||
|
mock_get_environment, mock_set_tenant,
|
||||||
|
mock_set_environment, mock_is, mock_client,
|
||||||
|
mock_package, mock_policy):
|
||||||
|
|
||||||
|
body = {"space_guid": "s1-p1",
|
||||||
|
"organization_guid": "o1-r1",
|
||||||
|
"plan_id": "p1-l1",
|
||||||
|
"service_id": "s1-e1",
|
||||||
|
"parameters": {'some_parameter': 'value',
|
||||||
|
'?': {}}}
|
||||||
|
self.request.body = json.dumps(body)
|
||||||
|
|
||||||
|
mock_package.return_value = mock.MagicMock()
|
||||||
|
mock_get_environment.side_effect = AttributeError
|
||||||
|
mock_get_tenant.side_effect = AttributeError
|
||||||
|
|
||||||
|
resp = self.controller.provision(self.request, {}, '111-111')
|
||||||
|
|
||||||
|
self.assertEqual({}, resp)
|
||||||
|
|
||||||
|
@mock.patch('murano.api.v1.cloudfoundry.cfapi.muranoclient')
|
||||||
|
@mock.patch('murano.api.v1.cloudfoundry.auth.authenticate')
|
||||||
|
@mock.patch('murano.db.services.cf_connections.get_service_for_instance')
|
||||||
|
def test_deprovision(self, mock_get_si, mock_auth, mock_client):
|
||||||
|
service = mock.MagicMock()
|
||||||
|
service.service_id = '111-111'
|
||||||
|
service.tenant_id = '222-222'
|
||||||
|
service.env_id = '333-333'
|
||||||
|
mock_get_si.return_value = service
|
||||||
|
|
||||||
|
resp = self.controller.deprovision(self.request, '555-555')
|
||||||
|
|
||||||
|
self.assertEqual({}, resp)
|
@ -47,6 +47,7 @@ console_scripts =
|
|||||||
murano-manage = murano.cmd.manage:main
|
murano-manage = murano.cmd.manage:main
|
||||||
murano-db-manage = murano.cmd.db_manage:main
|
murano-db-manage = murano.cmd.db_manage:main
|
||||||
murano-test-runner = murano.cmd.test_runner:main
|
murano-test-runner = murano.cmd.test_runner:main
|
||||||
|
murano-cfapi = murano.cmd.cfapi:main
|
||||||
oslo.config.opts =
|
oslo.config.opts =
|
||||||
murano = murano.opts:list_opts
|
murano = murano.opts:list_opts
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user