Site create API
Implement site create API. This patch only covers database model creation and aggregate creation. Partially implements: blueprint implement-api Change-Id: I299f367900b7b15ea992fe6f0eaf614f83a1a70e
This commit is contained in:
parent
a5fa51f6e4
commit
0caaa2b979
@ -77,8 +77,15 @@ function configure_tricircle_cascade_api {
|
|||||||
iniset $TRICIRCLE_CASCADE_API_CONF DEFAULT debug $ENABLE_DEBUG_LOG_LEVEL
|
iniset $TRICIRCLE_CASCADE_API_CONF DEFAULT debug $ENABLE_DEBUG_LOG_LEVEL
|
||||||
iniset $TRICIRCLE_CASCADE_API_CONF DEFAULT verbose True
|
iniset $TRICIRCLE_CASCADE_API_CONF DEFAULT verbose True
|
||||||
iniset $TRICIRCLE_CASCADE_API_CONF DEFAULT use_syslog $SYSLOG
|
iniset $TRICIRCLE_CASCADE_API_CONF DEFAULT use_syslog $SYSLOG
|
||||||
|
iniset $TRICIRCLE_CASCADE_API_CONF database connection `database_connection_url tricircle`
|
||||||
|
|
||||||
setup_colorized_logging $TRICIRCLE_CASCADE_API_CONF DEFAULT
|
iniset $TRICIRCLE_CASCADE_API_CONF client admin_username admin
|
||||||
|
iniset $TRICIRCLE_CASCADE_API_CONF client admin_password $ADMIN_PASSWORD
|
||||||
|
iniset $TRICIRCLE_CASCADE_API_CONF client admin_tenant demo
|
||||||
|
iniset $TRICIRCLE_CASCADE_API_CONF client auto_refresh_endpoint True
|
||||||
|
iniset $TRICIRCLE_CASCADE_API_CONF client top_site_name $OS_REGION_NAME
|
||||||
|
|
||||||
|
setup_colorized_logging $TRICIRCLE_CASCADE_API_CONF DEFAULT tenant_name
|
||||||
|
|
||||||
if is_service_enabled keystone; then
|
if is_service_enabled keystone; then
|
||||||
|
|
||||||
|
118
doc/source/api_v1.rst
Normal file
118
doc/source/api_v1.rst
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
================
|
||||||
|
Tricircle API v1
|
||||||
|
================
|
||||||
|
This API describes the ways of interacting with Tricircle(Cascade) service via
|
||||||
|
HTTP protocol using Representational State Transfer(ReST).
|
||||||
|
|
||||||
|
Application Root [/]
|
||||||
|
====================
|
||||||
|
Application Root provides links to all possible API methods for Tricircle. URLs
|
||||||
|
for other resources described below are relative to Application Root.
|
||||||
|
|
||||||
|
API v1 Root [/v1/]
|
||||||
|
==================
|
||||||
|
All API v1 URLs are relative to API v1 root.
|
||||||
|
|
||||||
|
Site [/sites/{site_id}]
|
||||||
|
=======================
|
||||||
|
A site represents a region in Keystone. When operating a site, Tricircle
|
||||||
|
decides the correct endpoints to send request based on the region of the site.
|
||||||
|
Considering the 2-layers architecture of Tricircle, we also have 2 kinds of
|
||||||
|
sites: top site and bottom site. A site has the following attributes:
|
||||||
|
|
||||||
|
- site_id
|
||||||
|
- site_name
|
||||||
|
- az_id
|
||||||
|
|
||||||
|
**site_id** is automatically generated when creating a site. **site_name** is
|
||||||
|
specified by user but **MUST** match the region name registered in Keystone.
|
||||||
|
When creating a bottom site, Tricircle automatically creates a host aggregate
|
||||||
|
and assigns the new availability zone id to **az_id**. Top site doesn't need a
|
||||||
|
host aggregate so **az_id** is left empty.
|
||||||
|
|
||||||
|
URL Parameters
|
||||||
|
--------------
|
||||||
|
- site_id: Site id
|
||||||
|
|
||||||
|
Models
|
||||||
|
------
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"site_id": "302e02a6-523c-4a92-a8d1-4939b31a788c",
|
||||||
|
"site_name": "Site1",
|
||||||
|
"az_id": "az_Site1"
|
||||||
|
}
|
||||||
|
|
||||||
|
Retrieve Site List [GET]
|
||||||
|
------------------------
|
||||||
|
- URL: /sites
|
||||||
|
- Status: 200
|
||||||
|
- Returns: List of Sites
|
||||||
|
|
||||||
|
Response
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"sites": [
|
||||||
|
{
|
||||||
|
"site_id": "f91ca3a5-d5c6-45d6-be4c-763f5a2c4aa3",
|
||||||
|
"site_name": "RegionOne",
|
||||||
|
"az_id": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"site_id": "302e02a6-523c-4a92-a8d1-4939b31a788c",
|
||||||
|
"site_name": "Site1",
|
||||||
|
"az_id": "az_Site1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Retrieve a Single Site [GET]
|
||||||
|
----------------------------
|
||||||
|
- URL: /sites/site_id
|
||||||
|
- Status: 200
|
||||||
|
- Returns: Site
|
||||||
|
|
||||||
|
Response
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"site": {
|
||||||
|
"site_id": "302e02a6-523c-4a92-a8d1-4939b31a788c",
|
||||||
|
"site_name": "Site1",
|
||||||
|
"az_id": "az_Site1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Create a Site [POST]
|
||||||
|
--------------------
|
||||||
|
- URL: /sites
|
||||||
|
- Status: 201
|
||||||
|
- Returns: Created Site
|
||||||
|
|
||||||
|
Request (application/json)
|
||||||
|
|
||||||
|
.. csv-table::
|
||||||
|
:header: "Parameter", "Type", "Description"
|
||||||
|
|
||||||
|
name, string, name of the Site
|
||||||
|
top, bool, "indicate whether it's a top Site, optional, default false"
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "RegionOne"
|
||||||
|
"top": true
|
||||||
|
}
|
||||||
|
|
||||||
|
Response
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"site": {
|
||||||
|
"site_id": "f91ca3a5-d5c6-45d6-be4c-763f5a2c4aa3",
|
||||||
|
"site_name": "RegionOne",
|
||||||
|
"az_id": ""
|
||||||
|
}
|
||||||
|
}
|
39
etc/api.conf
39
etc/api.conf
@ -179,6 +179,45 @@
|
|||||||
# If set, use this value for pool_timeout with sqlalchemy
|
# If set, use this value for pool_timeout with sqlalchemy
|
||||||
# pool_timeout = 10
|
# pool_timeout = 10
|
||||||
|
|
||||||
|
[client]
|
||||||
|
|
||||||
|
# Keystone authentication URL
|
||||||
|
# auth_url = http://127.0.0.1:5000/v3
|
||||||
|
|
||||||
|
# Keystone service URL
|
||||||
|
# identity_url = http://127.0.0.1:35357/v3
|
||||||
|
|
||||||
|
# If set to True, endpoint will be automatically refreshed if timeout
|
||||||
|
# accessing endpoint.
|
||||||
|
# auto_refresh_endpoint = False
|
||||||
|
|
||||||
|
# Name of top site which client needs to access
|
||||||
|
# top_site_name =
|
||||||
|
|
||||||
|
# Username of admin account for synchronizing endpoint with Keystone
|
||||||
|
# admin_username =
|
||||||
|
|
||||||
|
# Password of admin account for synchronizing endpoint with Keystone
|
||||||
|
# admin_password =
|
||||||
|
|
||||||
|
# Tenant name of admin account for synchronizing endpoint with Keystone
|
||||||
|
# admin_tenant =
|
||||||
|
|
||||||
|
# User domain name of admin account for synchronizing endpoint with Keystone
|
||||||
|
# admin_user_domain_name = default
|
||||||
|
|
||||||
|
# Tenant domain name of admin account for synchronizing endpoint with Keystone
|
||||||
|
# admin_tenant_domain_name = default
|
||||||
|
|
||||||
|
# Timeout for glance client in seconds
|
||||||
|
# glance_timeout = 60
|
||||||
|
|
||||||
|
# Timeout for neutron client in seconds
|
||||||
|
# neutron_timeout = 60
|
||||||
|
|
||||||
|
# Timeout for nova client in seconds
|
||||||
|
# nova_timeout = 60
|
||||||
|
|
||||||
[oslo_concurrency]
|
[oslo_concurrency]
|
||||||
|
|
||||||
# Directory to use for lock files. For security, the specified directory should
|
# Directory to use for lock files. For security, the specified directory should
|
||||||
|
@ -13,9 +13,20 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import oslo_log.log as logging
|
||||||
import pecan
|
import pecan
|
||||||
|
from pecan import request
|
||||||
from pecan import rest
|
from pecan import rest
|
||||||
|
|
||||||
|
import tricircle.context as t_context
|
||||||
|
from tricircle.db import client
|
||||||
|
from tricircle.db import exception
|
||||||
|
from tricircle.db import models
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def expose(*args, **kwargs):
|
def expose(*args, **kwargs):
|
||||||
kwargs.setdefault('content_type', 'application/json')
|
kwargs.setdefault('content_type', 'application/json')
|
||||||
@ -81,22 +92,101 @@ class V1Controller(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_context_from_environ(environ):
|
||||||
|
context_paras = {'auth_token': 'HTTP_X_AUTH_TOKEN',
|
||||||
|
'user': 'HTTP_X_USER_ID',
|
||||||
|
'tenant': 'HTTP_X_TENANT_ID',
|
||||||
|
'user_name': 'HTTP_X_USER_NAME',
|
||||||
|
'tenant_name': 'HTTP_X_PROJECT_NAME',
|
||||||
|
'domain': 'HTTP_X_DOMAIN_ID',
|
||||||
|
'user_domain': 'HTTP_X_USER_DOMAIN_ID',
|
||||||
|
'project_domain': 'HTTP_X_PROJECT_DOMAIN_ID',
|
||||||
|
'request_id': 'openstack.request_id'}
|
||||||
|
for key in context_paras:
|
||||||
|
context_paras[key] = environ.get(context_paras[key])
|
||||||
|
role = environ.get('HTTP_X_ROLE')
|
||||||
|
# TODO(zhiyuan): replace with policy check
|
||||||
|
context_paras['is_admin'] = role == 'admin'
|
||||||
|
return t_context.Context(**context_paras)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_environment():
|
||||||
|
return request.environ
|
||||||
|
|
||||||
|
|
||||||
class SitesController(rest.RestController):
|
class SitesController(rest.RestController):
|
||||||
|
"""ReST controller to handle CRUD operations of site resource"""
|
||||||
|
|
||||||
@expose(generic=True)
|
@expose()
|
||||||
def index(self):
|
def put(self, site_id, **kw):
|
||||||
if pecan.request.method != 'GET':
|
|
||||||
pecan.abort(405)
|
|
||||||
return {'message': 'GET'}
|
|
||||||
|
|
||||||
@when(index, method='PUT')
|
|
||||||
def put(self, **kw):
|
|
||||||
return {'message': 'PUT'}
|
return {'message': 'PUT'}
|
||||||
|
|
||||||
@when(index, method='POST')
|
@expose()
|
||||||
def post(self, **kw):
|
def get_one(self, site_id):
|
||||||
return {'message': 'POST'}
|
context = _extract_context_from_environ(_get_environment())
|
||||||
|
try:
|
||||||
|
return {'site': models.get_site(context, site_id)}
|
||||||
|
except exception.ResourceNotFound:
|
||||||
|
pecan.abort(404, 'Site with id %s not found' % site_id)
|
||||||
|
|
||||||
@when(index, method='DELETE')
|
@expose()
|
||||||
def delete(self):
|
def get_all(self):
|
||||||
|
context = _extract_context_from_environ(_get_environment())
|
||||||
|
sites = models.list_sites(context, [])
|
||||||
|
return {'sites': sites}
|
||||||
|
|
||||||
|
@expose()
|
||||||
|
def post(self, **kw):
|
||||||
|
context = _extract_context_from_environ(_get_environment())
|
||||||
|
if not context.is_admin:
|
||||||
|
pecan.abort(400, 'Admin role required to create sites')
|
||||||
|
return
|
||||||
|
|
||||||
|
site_name = kw.get('name')
|
||||||
|
is_top_site = kw.get('top', False)
|
||||||
|
|
||||||
|
if not site_name:
|
||||||
|
pecan.abort(400, 'Name of site required')
|
||||||
|
return
|
||||||
|
|
||||||
|
site_filters = [{'key': 'site_name', 'comparator': 'eq',
|
||||||
|
'value': site_name}]
|
||||||
|
sites = models.list_sites(context, site_filters)
|
||||||
|
if sites:
|
||||||
|
pecan.abort(409, 'Site with name %s exists' % site_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
ag_name = 'ag_%s' % site_name
|
||||||
|
# top site doesn't need az
|
||||||
|
az_name = 'az_%s' % site_name if not is_top_site else ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
site_dict = {'site_id': str(uuid.uuid4()),
|
||||||
|
'site_name': site_name,
|
||||||
|
'az_id': az_name}
|
||||||
|
site = models.create_site(context, site_dict)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.debug(e.message)
|
||||||
|
pecan.abort(500, 'Fail to create site')
|
||||||
|
return
|
||||||
|
|
||||||
|
# top site doesn't need aggregate
|
||||||
|
if is_top_site:
|
||||||
|
pecan.response.status = 201
|
||||||
|
return {'site': site}
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
top_client = client.Client()
|
||||||
|
top_client.create_aggregates(context, ag_name, az_name)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.debug(e.message)
|
||||||
|
# delete previously created site
|
||||||
|
models.delete_site(context, site['site_id'])
|
||||||
|
pecan.abort(500, 'Fail to create aggregate')
|
||||||
|
return
|
||||||
|
pecan.response.status = 201
|
||||||
|
return {'site': site}
|
||||||
|
|
||||||
|
@expose()
|
||||||
|
def delete(self, site_id):
|
||||||
return {'message': 'DELETE'}
|
return {'message': 'DELETE'}
|
||||||
|
@ -156,7 +156,7 @@ class Client(object):
|
|||||||
return region_service_endpoint_map
|
return region_service_endpoint_map
|
||||||
|
|
||||||
def _get_config_with_retry(self, cxt, filters, site, service, retry):
|
def _get_config_with_retry(self, cxt, filters, site, service, retry):
|
||||||
conf_list = models.list_site_service_configuration(cxt, filters)
|
conf_list = models.list_site_service_configurations(cxt, filters)
|
||||||
if len(conf_list) > 1:
|
if len(conf_list) > 1:
|
||||||
raise exception.EndpointNotUnique(site, service)
|
raise exception.EndpointNotUnique(site, service)
|
||||||
if len(conf_list) == 0:
|
if len(conf_list) == 0:
|
||||||
@ -204,12 +204,12 @@ class Client(object):
|
|||||||
endpoint_map = self._get_endpoint_from_keystone(admin_context)
|
endpoint_map = self._get_endpoint_from_keystone(admin_context)
|
||||||
else:
|
else:
|
||||||
endpoint_map = self._get_endpoint_from_keystone(cxt)
|
endpoint_map = self._get_endpoint_from_keystone(cxt)
|
||||||
|
|
||||||
for region in endpoint_map:
|
for region in endpoint_map:
|
||||||
# use region name to query site
|
# use region name to query site
|
||||||
site_filters = [{'key': 'site_name', 'comparator': 'eq',
|
site_filters = [{'key': 'site_name', 'comparator': 'eq',
|
||||||
'value': region}]
|
'value': region}]
|
||||||
site_list = models.list_sites(cxt, site_filters)
|
site_list = models.list_sites(cxt, site_filters)
|
||||||
|
|
||||||
# skip region/site not registered in cascade service
|
# skip region/site not registered in cascade service
|
||||||
if len(site_list) != 1:
|
if len(site_list) != 1:
|
||||||
continue
|
continue
|
||||||
@ -219,7 +219,7 @@ class Client(object):
|
|||||||
'value': site_id},
|
'value': site_id},
|
||||||
{'key': 'service_type', 'comparator': 'eq',
|
{'key': 'service_type', 'comparator': 'eq',
|
||||||
'value': service}]
|
'value': service}]
|
||||||
config_list = models.list_site_service_configuration(
|
config_list = models.list_site_service_configurations(
|
||||||
cxt, config_filters)
|
cxt, config_filters)
|
||||||
|
|
||||||
if len(config_list) > 1:
|
if len(config_list) > 1:
|
||||||
@ -234,7 +234,6 @@ class Client(object):
|
|||||||
config_dict = {
|
config_dict = {
|
||||||
'service_id': str(uuid.uuid4()),
|
'service_id': str(uuid.uuid4()),
|
||||||
'site_id': site_id,
|
'site_id': site_id,
|
||||||
'service_name': '%s_%s' % (region, service),
|
|
||||||
'service_type': service,
|
'service_type': service,
|
||||||
'service_url': endpoint_map[region][service]
|
'service_url': endpoint_map[region][service]
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,9 @@ def _get_resource(context, model, pk_value):
|
|||||||
def create_resource(context, model, res_dict):
|
def create_resource(context, model, res_dict):
|
||||||
res_obj = model.from_dict(res_dict)
|
res_obj = model.from_dict(res_dict)
|
||||||
context.session.add(res_obj)
|
context.session.add(res_obj)
|
||||||
|
context.session.flush()
|
||||||
|
# retrieve auto-generated fields
|
||||||
|
context.session.refresh(res_obj)
|
||||||
return res_obj.to_dict()
|
return res_obj.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,18 +34,10 @@ def upgrade(migrate_engine):
|
|||||||
'cascaded_site_service_configuration', meta,
|
'cascaded_site_service_configuration', meta,
|
||||||
sql.Column('service_id', sql.String(length=64), primary_key=True),
|
sql.Column('service_id', sql.String(length=64), primary_key=True),
|
||||||
sql.Column('site_id', sql.String(length=64), nullable=False),
|
sql.Column('site_id', sql.String(length=64), nullable=False),
|
||||||
sql.Column('service_name', sql.String(length=64), unique=True,
|
|
||||||
nullable=False),
|
|
||||||
sql.Column('service_type', sql.String(length=64), nullable=False),
|
sql.Column('service_type', sql.String(length=64), nullable=False),
|
||||||
sql.Column('service_url', sql.String(length=512), nullable=False),
|
sql.Column('service_url', sql.String(length=512), nullable=False),
|
||||||
mysql_engine='InnoDB',
|
mysql_engine='InnoDB',
|
||||||
mysql_charset='utf8')
|
mysql_charset='utf8')
|
||||||
cascaded_service_types = sql.Table(
|
|
||||||
'cascaded_service_types', meta,
|
|
||||||
sql.Column('id', sql.Integer, primary_key=True),
|
|
||||||
sql.Column('service_type', sql.String(length=64), unique=True),
|
|
||||||
mysql_engine='InnoDB',
|
|
||||||
mysql_charset='utf8')
|
|
||||||
cascaded_site_services = sql.Table(
|
cascaded_site_services = sql.Table(
|
||||||
'cascaded_site_services', meta,
|
'cascaded_site_services', meta,
|
||||||
sql.Column('site_id', sql.String(length=64), primary_key=True),
|
sql.Column('site_id', sql.String(length=64), primary_key=True),
|
||||||
@ -53,20 +45,15 @@ def upgrade(migrate_engine):
|
|||||||
mysql_charset='utf8')
|
mysql_charset='utf8')
|
||||||
|
|
||||||
tables = [cascaded_sites, cascaded_site_service_configuration,
|
tables = [cascaded_sites, cascaded_site_service_configuration,
|
||||||
cascaded_service_types, cascaded_site_services]
|
cascaded_site_services]
|
||||||
for table in tables:
|
for table in tables:
|
||||||
table.create()
|
table.create()
|
||||||
|
|
||||||
fkeys = [
|
fkey = {'columns': [cascaded_site_service_configuration.c.site_id],
|
||||||
{'columns': [cascaded_site_service_configuration.c.site_id],
|
'references': [cascaded_sites.c.site_id]}
|
||||||
'references': [cascaded_sites.c.site_id]},
|
migrate.ForeignKeyConstraint(columns=fkey['columns'],
|
||||||
{'columns': [cascaded_site_service_configuration.c.service_type],
|
refcolumns=fkey['references'],
|
||||||
'references': [cascaded_service_types.c.service_type]}
|
name=fkey.get('name')).create()
|
||||||
]
|
|
||||||
for fkey in fkeys:
|
|
||||||
migrate.ForeignKeyConstraint(columns=fkey['columns'],
|
|
||||||
refcolumns=fkey['references'],
|
|
||||||
name=fkey.get('name')).create()
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade(migrate_engine):
|
def downgrade(migrate_engine):
|
||||||
|
@ -44,11 +44,6 @@ def update_site(context, site_id, update_dict):
|
|||||||
return core.update_resource(context, Site, site_id, update_dict)
|
return core.update_resource(context, Site, site_id, update_dict)
|
||||||
|
|
||||||
|
|
||||||
def create_service_type(context, type_dict):
|
|
||||||
with context.session.begin():
|
|
||||||
return core.create_resource(context, ServiceType, type_dict)
|
|
||||||
|
|
||||||
|
|
||||||
def create_site_service_configuration(context, config_dict):
|
def create_site_service_configuration(context, config_dict):
|
||||||
with context.session.begin():
|
with context.session.begin():
|
||||||
return core.create_resource(context, SiteServiceConfiguration,
|
return core.create_resource(context, SiteServiceConfiguration,
|
||||||
@ -61,7 +56,7 @@ def delete_site_service_configuration(context, config_id):
|
|||||||
SiteServiceConfiguration, config_id)
|
SiteServiceConfiguration, config_id)
|
||||||
|
|
||||||
|
|
||||||
def list_site_service_configuration(context, filters):
|
def list_site_service_configurations(context, filters):
|
||||||
with context.session.begin():
|
with context.session.begin():
|
||||||
return core.query_resource(context, SiteServiceConfiguration, filters)
|
return core.query_resource(context, SiteServiceConfiguration, filters)
|
||||||
|
|
||||||
@ -83,31 +78,18 @@ class Site(core.ModelBase, core.DictBase):
|
|||||||
|
|
||||||
class SiteServiceConfiguration(core.ModelBase, core.DictBase):
|
class SiteServiceConfiguration(core.ModelBase, core.DictBase):
|
||||||
__tablename__ = 'cascaded_site_service_configuration'
|
__tablename__ = 'cascaded_site_service_configuration'
|
||||||
attributes = ['service_id', 'site_id', 'service_name',
|
attributes = ['service_id', 'site_id', 'service_type', 'service_url']
|
||||||
'service_type', 'service_url']
|
|
||||||
service_id = sql.Column('service_id', sql.String(length=64),
|
service_id = sql.Column('service_id', sql.String(length=64),
|
||||||
primary_key=True)
|
primary_key=True)
|
||||||
site_id = sql.Column('site_id', sql.String(length=64),
|
site_id = sql.Column('site_id', sql.String(length=64),
|
||||||
sql.ForeignKey('cascaded_sites.site_id'),
|
sql.ForeignKey('cascaded_sites.site_id'),
|
||||||
nullable=False)
|
nullable=False)
|
||||||
service_name = sql.Column('service_name', sql.String(length=64),
|
service_type = sql.Column('service_type', sql.String(length=64),
|
||||||
unique=True, nullable=False)
|
nullable=False)
|
||||||
service_type = sql.Column(
|
|
||||||
'service_type', sql.String(length=64),
|
|
||||||
sql.ForeignKey('cascaded_service_types.service_type'),
|
|
||||||
nullable=False)
|
|
||||||
service_url = sql.Column('service_url', sql.String(length=512),
|
service_url = sql.Column('service_url', sql.String(length=512),
|
||||||
nullable=False)
|
nullable=False)
|
||||||
|
|
||||||
|
|
||||||
class ServiceType(core.ModelBase, core.DictBase):
|
|
||||||
__tablename__ = 'cascaded_service_types'
|
|
||||||
attributes = ['id', 'service_type']
|
|
||||||
id = sql.Column('id', sql.Integer, primary_key=True)
|
|
||||||
service_type = sql.Column('service_type', sql.String(length=64),
|
|
||||||
unique=True)
|
|
||||||
|
|
||||||
|
|
||||||
class SiteService(core.ModelBase, core.DictBase):
|
class SiteService(core.ModelBase, core.DictBase):
|
||||||
__tablename__ = 'cascaded_site_services'
|
__tablename__ = 'cascaded_site_services'
|
||||||
attributes = ['site_id']
|
attributes = ['site_id']
|
||||||
|
0
tricircle/tests/unit/api/__init__.py
Normal file
0
tricircle/tests/unit/api/__init__.py
Normal file
0
tricircle/tests/unit/api/controllers/__init__.py
Normal file
0
tricircle/tests/unit/api/controllers/__init__.py
Normal file
150
tricircle/tests/unit/api/controllers/test_root.py
Normal file
150
tricircle/tests/unit/api/controllers/test_root.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# Copyright 2015 Huawei Technologies Co., Ltd.
|
||||||
|
# All Rights Reserved
|
||||||
|
#
|
||||||
|
# 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 mock
|
||||||
|
from mock import patch
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
|
||||||
|
import tricircle.api.controllers.root as root_controller
|
||||||
|
from tricircle import context
|
||||||
|
from tricircle.db import client
|
||||||
|
from tricircle.db import core
|
||||||
|
from tricircle.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class ControllerTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
core.initialize()
|
||||||
|
core.ModelBase.metadata.create_all(core.get_engine())
|
||||||
|
self.context = context.Context()
|
||||||
|
self.context.is_admin = True
|
||||||
|
|
||||||
|
root_controller._get_environment = mock.Mock(return_value={})
|
||||||
|
root_controller._extract_context_from_environ = mock.Mock(
|
||||||
|
return_value=self.context)
|
||||||
|
|
||||||
|
pecan.abort = mock.Mock()
|
||||||
|
pecan.response = mock.Mock()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
core.ModelBase.metadata.drop_all(core.get_engine())
|
||||||
|
|
||||||
|
|
||||||
|
class SitesControllerTest(ControllerTest):
|
||||||
|
def setUp(self):
|
||||||
|
super(SitesControllerTest, self).setUp()
|
||||||
|
self.controller = root_controller.SitesController()
|
||||||
|
|
||||||
|
def test_post_top_site(self):
|
||||||
|
kw = {'name': 'TopSite', 'top': True}
|
||||||
|
site_id = self.controller.post(**kw)['site']['site_id']
|
||||||
|
site = models.get_site(self.context, site_id)
|
||||||
|
self.assertEqual(site['site_name'], 'TopSite')
|
||||||
|
self.assertEqual(site['az_id'], '')
|
||||||
|
|
||||||
|
@patch.object(client.Client, 'create_resources')
|
||||||
|
def test_post_bottom_site(self, mock_method):
|
||||||
|
kw = {'name': 'BottomSite'}
|
||||||
|
site_id = self.controller.post(**kw)['site']['site_id']
|
||||||
|
site = models.get_site(self.context, site_id)
|
||||||
|
self.assertEqual(site['site_name'], 'BottomSite')
|
||||||
|
self.assertEqual(site['az_id'], 'az_BottomSite')
|
||||||
|
mock_method.assert_called_once_with('aggregate', self.context,
|
||||||
|
'ag_BottomSite', 'az_BottomSite')
|
||||||
|
|
||||||
|
def test_post_site_name_missing(self):
|
||||||
|
kw = {'top': True}
|
||||||
|
self.controller.post(**kw)
|
||||||
|
pecan.abort.assert_called_once_with(400, 'Name of site required')
|
||||||
|
|
||||||
|
def test_post_conflict(self):
|
||||||
|
kw = {'name': 'TopSite', 'top': True}
|
||||||
|
self.controller.post(**kw)
|
||||||
|
self.controller.post(**kw)
|
||||||
|
pecan.abort.assert_called_once_with(409,
|
||||||
|
'Site with name TopSite exists')
|
||||||
|
|
||||||
|
def test_post_not_admin(self):
|
||||||
|
self.context.is_admin = False
|
||||||
|
kw = {'name': 'TopSite', 'top': True}
|
||||||
|
self.controller.post(**kw)
|
||||||
|
pecan.abort.assert_called_once_with(
|
||||||
|
400, 'Admin role required to create sites')
|
||||||
|
|
||||||
|
@patch.object(client.Client, 'create_resources')
|
||||||
|
def test_post_decide_top(self, mock_method):
|
||||||
|
# 'top' default to False
|
||||||
|
# top site
|
||||||
|
kw = {'name': 'Site1', 'top': True}
|
||||||
|
self.controller.post(**kw)
|
||||||
|
# bottom site
|
||||||
|
kw = {'name': 'Site2', 'top': False}
|
||||||
|
self.controller.post(**kw)
|
||||||
|
kw = {'name': 'Site3'}
|
||||||
|
self.controller.post(**kw)
|
||||||
|
calls = [mock.call('aggregate', self.context, 'ag_Site%d' % i,
|
||||||
|
'az_Site%d' % i) for i in xrange(2, 4)]
|
||||||
|
mock_method.assert_has_calls(calls)
|
||||||
|
|
||||||
|
@patch.object(models, 'create_site')
|
||||||
|
def test_post_create_site_exception(self, mock_method):
|
||||||
|
mock_method.side_effect = Exception
|
||||||
|
kw = {'name': 'BottomSite'}
|
||||||
|
self.controller.post(**kw)
|
||||||
|
pecan.abort.assert_called_once_with(500, 'Fail to create site')
|
||||||
|
|
||||||
|
@patch.object(client.Client, 'create_resources')
|
||||||
|
def test_post_create_aggregate_exception(self, mock_method):
|
||||||
|
mock_method.side_effect = Exception
|
||||||
|
kw = {'name': 'BottomSite'}
|
||||||
|
self.controller.post(**kw)
|
||||||
|
pecan.abort.assert_called_once_with(500, 'Fail to create aggregate')
|
||||||
|
|
||||||
|
# make sure site is deleted
|
||||||
|
site_filter = [{'key': 'site_name',
|
||||||
|
'comparator': 'eq',
|
||||||
|
'value': 'BottomSite'}]
|
||||||
|
sites = models.list_sites(self.context, site_filter)
|
||||||
|
self.assertEqual(len(sites), 0)
|
||||||
|
|
||||||
|
def test_get_one(self):
|
||||||
|
kw = {'name': 'TopSite', 'top': True}
|
||||||
|
site_id = self.controller.post(**kw)['site']['site_id']
|
||||||
|
return_site = self.controller.get_one(site_id)['site']
|
||||||
|
self.assertEqual(return_site, {'site_id': site_id,
|
||||||
|
'site_name': 'TopSite',
|
||||||
|
'az_id': ''})
|
||||||
|
|
||||||
|
def test_get_one_not_found(self):
|
||||||
|
self.controller.get_one('fake_id')
|
||||||
|
pecan.abort.assert_called_once_with(404,
|
||||||
|
'Site with id fake_id not found')
|
||||||
|
|
||||||
|
@patch.object(client.Client, 'create_resources', new=mock.Mock)
|
||||||
|
def test_get_all(self):
|
||||||
|
kw1 = {'name': 'TopSite', 'top': True}
|
||||||
|
kw2 = {'name': 'BottomSite'}
|
||||||
|
self.controller.post(**kw1)
|
||||||
|
self.controller.post(**kw2)
|
||||||
|
sites = self.controller.get_all()
|
||||||
|
actual_result = [(site['site_name'],
|
||||||
|
site['az_id']) for site in sites['sites']]
|
||||||
|
expect_result = [('BottomSite', 'az_BottomSite'), ('TopSite', '')]
|
||||||
|
self.assertItemsEqual(actual_result, expect_result)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
core.ModelBase.metadata.drop_all(core.get_engine())
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
import uuid
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
@ -31,7 +32,6 @@ FAKE_RESOURCE = 'fake_res'
|
|||||||
FAKE_SITE_ID = 'fake_site_id'
|
FAKE_SITE_ID = 'fake_site_id'
|
||||||
FAKE_SITE_NAME = 'fake_site_name'
|
FAKE_SITE_NAME = 'fake_site_name'
|
||||||
FAKE_SERVICE_ID = 'fake_service_id'
|
FAKE_SERVICE_ID = 'fake_service_id'
|
||||||
FAKE_SERVICE_NAME = 'fake_service_name'
|
|
||||||
FAKE_TYPE = 'fake_type'
|
FAKE_TYPE = 'fake_type'
|
||||||
FAKE_URL = 'http://127.0.0.1:12345'
|
FAKE_URL = 'http://127.0.0.1:12345'
|
||||||
FAKE_URL_INVALID = 'http://127.0.0.1:23456'
|
FAKE_URL_INVALID = 'http://127.0.0.1:23456'
|
||||||
@ -105,6 +105,8 @@ class ClientTest(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
core.initialize()
|
core.initialize()
|
||||||
core.ModelBase.metadata.create_all(core.get_engine())
|
core.ModelBase.metadata.create_all(core.get_engine())
|
||||||
|
# enforce foreign key constraint for sqlite
|
||||||
|
core.get_engine().execute('pragma foreign_keys=on')
|
||||||
self.context = context.Context()
|
self.context = context.Context()
|
||||||
|
|
||||||
site_dict = {
|
site_dict = {
|
||||||
@ -112,19 +114,13 @@ class ClientTest(unittest.TestCase):
|
|||||||
'site_name': FAKE_SITE_NAME,
|
'site_name': FAKE_SITE_NAME,
|
||||||
'az_id': FAKE_AZ
|
'az_id': FAKE_AZ
|
||||||
}
|
}
|
||||||
type_dict = {
|
|
||||||
'id': 1,
|
|
||||||
'service_type': FAKE_TYPE
|
|
||||||
}
|
|
||||||
config_dict = {
|
config_dict = {
|
||||||
'service_id': FAKE_SERVICE_ID,
|
'service_id': FAKE_SERVICE_ID,
|
||||||
'site_id': FAKE_SITE_ID,
|
'site_id': FAKE_SITE_ID,
|
||||||
'service_name': FAKE_SERVICE_NAME,
|
|
||||||
'service_type': FAKE_TYPE,
|
'service_type': FAKE_TYPE,
|
||||||
'service_url': FAKE_URL
|
'service_url': FAKE_URL
|
||||||
}
|
}
|
||||||
models.create_site(self.context, site_dict)
|
models.create_site(self.context, site_dict)
|
||||||
models.create_service_type(self.context, type_dict)
|
|
||||||
models.create_site_service_configuration(self.context, config_dict)
|
models.create_site_service_configuration(self.context, config_dict)
|
||||||
|
|
||||||
global FAKE_RESOURCES
|
global FAKE_RESOURCES
|
||||||
@ -207,7 +203,6 @@ class ClientTest(unittest.TestCase):
|
|||||||
config_dict = {
|
config_dict = {
|
||||||
'service_id': FAKE_SERVICE_ID + '_new',
|
'service_id': FAKE_SERVICE_ID + '_new',
|
||||||
'site_id': FAKE_SITE_ID,
|
'site_id': FAKE_SITE_ID,
|
||||||
'service_name': FAKE_SERVICE_NAME + '_new',
|
|
||||||
'service_type': FAKE_TYPE,
|
'service_type': FAKE_TYPE,
|
||||||
'service_url': FAKE_URL
|
'service_url': FAKE_URL
|
||||||
}
|
}
|
||||||
@ -249,6 +244,31 @@ class ClientTest(unittest.TestCase):
|
|||||||
FAKE_RESOURCE, self.context, [])
|
FAKE_RESOURCE, self.context, [])
|
||||||
self.assertEqual(resources, [{'name': 'res1'}, {'name': 'res2'}])
|
self.assertEqual(resources, [{'name': 'res1'}, {'name': 'res2'}])
|
||||||
|
|
||||||
|
def test_update_endpoint_from_keystone(self):
|
||||||
|
self.client._get_admin_token = mock.Mock()
|
||||||
|
self.client._get_endpoint_from_keystone = mock.Mock()
|
||||||
|
self.client._get_endpoint_from_keystone.return_value = {
|
||||||
|
FAKE_SITE_NAME: {FAKE_TYPE: FAKE_URL,
|
||||||
|
'another_fake_type': 'http://127.0.0.1:34567'},
|
||||||
|
'not_registered_site': {FAKE_TYPE: FAKE_URL}
|
||||||
|
}
|
||||||
|
models.create_site_service_configuration = mock.Mock()
|
||||||
|
models.update_site_service_configuration = mock.Mock()
|
||||||
|
uuid.uuid4 = mock.Mock()
|
||||||
|
uuid.uuid4.return_value = 'another_fake_service_id'
|
||||||
|
|
||||||
|
self.client.update_endpoint_from_keystone(self.context)
|
||||||
|
update_dict = {'service_url': FAKE_URL}
|
||||||
|
create_dict = {'service_id': 'another_fake_service_id',
|
||||||
|
'site_id': FAKE_SITE_ID,
|
||||||
|
'service_type': 'another_fake_type',
|
||||||
|
'service_url': 'http://127.0.0.1:34567'}
|
||||||
|
# not registered site is skipped
|
||||||
|
models.update_site_service_configuration.assert_called_once_with(
|
||||||
|
self.context, FAKE_SERVICE_ID, update_dict)
|
||||||
|
models.create_site_service_configuration.assert_called_once_with(
|
||||||
|
self.context, create_dict)
|
||||||
|
|
||||||
def test_get_endpoint(self):
|
def test_get_endpoint(self):
|
||||||
cfg.CONF.set_override(name='auto_refresh_endpoint', override=False,
|
cfg.CONF.set_override(name='auto_refresh_endpoint', override=False,
|
||||||
group='client')
|
group='client')
|
||||||
|
@ -43,15 +43,9 @@ class ModelsTest(unittest.TestCase):
|
|||||||
site_ret = models.create_site(self.context, site)
|
site_ret = models.create_site(self.context, site)
|
||||||
self.assertEqual(site_ret, site)
|
self.assertEqual(site_ret, site)
|
||||||
|
|
||||||
service_type = {'id': 1,
|
|
||||||
'service_type': 'nova'}
|
|
||||||
type_ret = models.create_service_type(self.context, service_type)
|
|
||||||
self.assertEqual(type_ret, service_type)
|
|
||||||
|
|
||||||
configuration = {
|
configuration = {
|
||||||
'service_id': 'test_config_uuid',
|
'service_id': 'test_config_uuid',
|
||||||
'site_id': 'test_site_uuid',
|
'site_id': 'test_site_uuid',
|
||||||
'service_name': 'nova_service',
|
|
||||||
'service_type': 'nova',
|
'service_type': 'nova',
|
||||||
'service_url': 'http://test_url'
|
'service_url': 'http://test_url'
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user