Browse Source
Implement site create API. This patch only covers database model creation and aggregate creation. Partially implements: blueprint implement-api Change-Id: I299f367900b7b15ea992fe6f0eaf614f83a1a70echanges/40/229240/3
13 changed files with 461 additions and 72 deletions
@ -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": "" |
||||
} |
||||
} |
@ -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()) |
Loading…
Reference in new issue