Implement category management API

Adds new API calls, responsible for add,
browse and delete categories.

Implements blueprint enable-category-management

Change-Id: I9da0680cfa244ef225be0706a54f492644c0dcba
This commit is contained in:
Ekaterina Chernova 2014-12-31 16:02:43 +03:00
parent 42c320a085
commit 2c23f73e72
8 changed files with 372 additions and 14 deletions

View File

@ -25,7 +25,8 @@ General information
Murano Service API is a programmatic interface used for interaction with
Murano. Other interaction mechanisms like Murano Dashboard or Murano CLI
should use API as underlying protocol for interaction.
* **Allowed HTTPs requests**
* **Allowed HTTPs requests**
* *POST* : To create a resource
* *GET* : Get a resource or list of resources

View File

@ -235,6 +235,10 @@ See the full specification `here <http://tools.ietf.org/html/rfc6902>`_.
``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
**Content type**
application/murano-packages-json-patch
Allowed operations:
::
@ -305,7 +309,7 @@ Delete application definition from the catalog
**Parameters**
``id`` (required) Hexadecimal `id` (or fully qualified name) of the package to delete
* ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package to delete
**Response 404**
@ -321,7 +325,7 @@ Get application definition package
**Parameters**
* id (required) Hexadecimal `id` (or fully qualified name) of the package
* ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
**Response 200 (application/octetstream)**
@ -341,7 +345,7 @@ Retrieve UI definition for a application which described in a package with provi
**Parameters**
* id (required) Hexadecimal `id` (or fully qualified name) of the package
* ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
**Response 200 (application/octet-stream)**
@ -369,7 +373,7 @@ Retrieve application logo which described in a package with provided id
**Parameters**
id (required) Hexadecimal `id` (or fully qualified name) of the package
``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
**Response 200 (application/octet-stream)**
@ -377,26 +381,208 @@ The sequence of bytes representing application logo
**Response 403**
Specified package is not public and not owned by user tenant, performing the request
Specified package is not public and not owned by user tenant,
performing the request
**Response 404**
Specified package is not public and not owned by user tenant, performing the request
Specified package is not public and not owned by user tenant,
performing the request
Categories
==========
Provides category management. Categories are used in the Application Catalog
to group application for easy browsing and search.
List categories
---------------
`/v1/catalog/packages/categories [GET]`
* `/v1/catalog/packages/categories [GET]`
Retrieve list of all available application categories
!DEPRECATED (Plan to remove in L release) Retrieve list of all available application categories
**Response 200 (application/json)**
**Response 200 (application/json)**
::
A list, containing category names
*Content-Type*
application/json
::
{
"categories": ["Web service", "Directory", "Database", "Storage"]
}
* `/v1/catalog/categories [GET]`
+----------+----------------------------------+----------------------------------+
| Method | URI | Description |
+==========+==================================+==================================+
| GET | /catalog/categories | Get list of existing categories |
+----------+----------------------------------+----------------------------------+
Retrieve list of all available application categories
**Response 200 (application/json)**
A list, containing detailed information about each category
*Content-Type*
application/json
::
{"categories": [
{
"id": "0420045dce7445fabae7e5e61fff9e2f",
"updated": "2014-12-26T13:57:04",
"name": "Web",
"created": "2014-12-26T13:57:04"
},
{
"id": "3dd486b1e26f40ac8f35416b63f52042",
"updated": "2014-12-26T13:57:04",
"name": "Databases",
"created": "2014-12-26T13:57:04"
}]
}
Get category details
--------------------
`/catalog/categories/<category_id> [GET]`
Return detailed information for a provided category
*Request*
+----------+-----------------------------------+----------------------------------+
| Method | URI | Description |
+==========+===================================+==================================+
| GET | /catalog/categories/<category_id> | Get category detail |
+----------+-----------------------------------+----------------------------------+
*Parameters*
* ``category_id`` - required, category ID, required
*Response*
*Content-Type*
application/json
::
{
"id": "b308f7fa8a2f4a5eb419970c827f4466",
"updated": "2015-01-28T17:00:19",
"packages": [
{
"fully_qualified_name": "io.murano.apps.ZabbixServer",
"id": "4dfb566e69e6445fbd4aea5099fe95e9",
"name": "Zabbix Server"
}
],
"name": "Web",
"created": "2015-01-28T17:00:19"
}
+----------------+-----------------------------------------------------------+
| Code | Description |
+================+===========================================================+
| 200 | OK. Category deleted successfully |
+----------------+-----------------------------------------------------------+
| 401 | User is not authorized to access this session |
+----------------+-----------------------------------------------------------+
| 404 | Not found. Specified category doesn`t exist |
+----------------+-----------------------------------------------------------+
Add new category
----------------
`/catalog/categories [POST]`
Add new category to the Application Catalog
*Parameters*
+----------------------+------------+--------------------------------------------------------+
| Attribute | Type | Description |
+======================+============+========================================================+
| name | string | Environment name; only alphanumeric characters and '-' |
+----------------------+------------+--------------------------------------------------------+
*Request*
+----------+----------------------------------+----------------------------------+
| Method | URI | Description |
+==========+==================================+==================================+
| POST | /catalog/categories | Create new category |
+----------+----------------------------------+----------------------------------+
*Content-Type*
application/json
*Example*
{"name": "category_name"}
*Response*
::
{
"id": "ce373a477f211e187a55404a662f968",
"name": "category_name",
"created": "2013-11-30T03:23:42Z",
"updated": "2013-11-30T03:23:44Z",
}
+----------------+-----------------------------------------------------------+
| Code | Description |
+================+===========================================================+
| 200 | OK. Category created successfully |
+----------------+-----------------------------------------------------------+
| 401 | User is not authorized to access this session |
+----------------+-----------------------------------------------------------+
| 409 | Conflict. Category with specified name already exist |
+----------------+-----------------------------------------------------------+
Delete category
---------------
`/catalog/categories [DELETE]`
*Request*
+----------+-----------------------------------+-----------------------------------+
| Method | URI | Description |
+==========+===================================+===================================+
| DELETE | /catalog/categories/<category_id> | Delete category with specified id |
+----------+-----------------------------------+-----------------------------------+
*Parameters:*
* ``category_id`` - required, category ID, required
*Response*
+----------------+-----------------------------------------------------------+
| Code | Description |
+================+===========================================================+
| 200 | OK. Category deleted successfully |
+----------------+-----------------------------------------------------------+
| 401 | User is not authorized to access this session |
+----------------+-----------------------------------------------------------+
| 404 | Not found. Specified category doesn`t exist |
+----------------+-----------------------------------------------------------+
| 403 | Forbidden. Category with specified name is assigned to |
| | the package, presented in the catalog |
+----------------+-----------------------------------------------------------+

View File

@ -5,6 +5,8 @@
"update_package": "rule:admin_api",
"upload_package": "rule:admin_api",
"delete_package": "rule:admin_api"
"delete_package": "rule:admin_api",
"delete_category": "rule:admin_api",
"add_category": "rule:admin_api"
}

View File

@ -268,12 +268,45 @@ class Controller(object):
db_api.package_delete(package_id, req.context)
def get_category(self, req, category_id):
policy.check("get_category", req.context)
category = db_api.category_get(category_id, packages=True)
return category.to_dict()
def show_categories(self, req):
policy.check("show_categories", req.context)
categories = db_api.categories_list()
return {'categories': [category.name for category in categories]}
def list_categories(self, req):
policy.check("list_categories", req.context)
categories = db_api.categories_list()
return {'categories': [category.to_dict() for category in categories]}
def add_category(self, req, body=None):
policy.check("add_category", req.context)
if not body.get('name'):
raise exc.HTTPBadRequest(
explanation='Please, specify a name of the category to create')
try:
category = db_api.category_add(body['name'])
except db_exc.DBDuplicateEntry:
msg = _('Category with specified name is already exist')
LOG.error(msg)
raise exc.HTTPConflict(explanation=msg)
return category.to_dict()
def delete_category(self, req, category_id):
target = {'category_id': category_id}
policy.check("delete_category", req.context, target)
category = db_api.category_get(category_id, packages=True)
if category.packages:
msg = _("It's impossible to delete categories assigned"
" to the package, uploaded to the catalog")
raise exc.HTTPForbidden(explanation=msg)
db_api.category_delete(category_id)
class PackageSerializer(wsgi.ResponseSerializer):
def serialize(self, action_result, accept, action):

View File

@ -235,7 +235,22 @@ class API(wsgi.Router):
controller=catalog_resource,
action='download',
conditions={'method': ['GET']})
mapper.connect('/catalog/categories',
controller=catalog_resource,
action='list_categories',
conditions={'method': ['GET']})
mapper.connect('/catalog/categories/{category_id}',
controller=catalog_resource,
action='get_category',
conditions={'method': ['GET']})
mapper.connect('/catalog/categories',
controller=catalog_resource,
action='add_category',
conditions={'method': ['POST']})
mapper.connect('/catalog/categories/{category_id}',
controller=catalog_resource,
action='delete_category',
conditions={'method': ['DELETE']})
req_stats_resource = request_statistics.create_resource()
mapper.connect('/stats',
controller=req_stats_resource,

View File

@ -200,6 +200,27 @@ def _do_remove(package, change):
return package
def _get_packages_for_category(session, category_id):
"""Return detailed list of packages, belonging to the provided category
:param session:
:param category_id:
:return: list of dictionaries, containing id, name and package frn
"""
pkg = models.Package
packages = (session.query(pkg.id, pkg.name, pkg.fully_qualified_name)
.filter(pkg.categories
.any(models.Category.id == category_id))
.all())
result_packages = []
for package in packages:
id, name, fqn = package
result_packages.append({'id': id,
'name': name,
'fully_qualified_name': fqn})
return result_packages
def package_update(pkg_id_or_name, changes, context):
"""Update package information
:param changes: parameters to update
@ -357,6 +378,24 @@ def package_delete(package_id_or_name, context):
session.delete(package)
def category_get(category_id, session=None, packages=False):
"""Return category details
:param category_id: ID of a category, string
:returns: detailed information about category, dict
"""
if not session:
session = db_session.get_session()
category = session.query(models.Category).get(category_id)
if not category:
msg = _("Category id or '{0}' not found").format(category_id)
LOG.error(msg)
raise exc.HTTPNotFound(msg)
if packages:
category.packages = _get_packages_for_category(session, category_id)
return category
def categories_list():
session = db_session.get_session()
return session.query(models.Category).all()
@ -380,3 +419,16 @@ def category_add(category_name):
category.save(session)
return category
def category_delete(category_id):
"""Delete a category by ID."""
session = db_session.get_session()
with session.begin():
category = session.query(models.Category).get(category_id)
if not category:
msg = _("Category id '{0}' not found").format(category_id)
LOG.error(msg)
raise exc.HTTPNotFound(msg)
session.delete(category)

View File

@ -285,6 +285,10 @@ class Category(Base, TimestampMixin):
default=uuidutils.generate_uuid)
name = sa.Column(sa.String(80), nullable=False, index=True, unique=True)
def to_dict(self):
dictionary = super(Category, self).to_dict()
return dictionary
class Tag(Base, TimestampMixin):
"""Represents tags in the datastore."""

View File

@ -16,15 +16,19 @@
import cgi
import cStringIO
import imghdr
import json
import os
import mock
from oslo.utils import timeutils
from murano.api.v1 import catalog
from murano.common import policy
from murano.db.catalog import api as db_catalog_api
from murano.db import models
from murano.packages import load_utils
import murano.tests.unit.api.base as test_base
import murano.tests.unit.utils as test_utils
class TestCatalogApi(test_base.ControllerTest, test_base.MuranoApiTestCase):
@ -132,3 +136,64 @@ Content-Type: application/json
content_type='multipart/form-data; ; boundary=BOUNDARY',
params={"is_public": "true"})
res = req.get_response(self.api)
self.assertEqual(403, res.status_code)
def test_add_category(self):
"""Check that category added successfully
"""
self._set_policy_rules({'add_category': '@'})
self.expect_policy_check('add_category')
fake_now = timeutils.utcnow()
timeutils.utcnow.override_time = fake_now
expected = {'name': 'new_category',
'created': timeutils.isotime(fake_now)[:-1],
'updated': timeutils.isotime(fake_now)[:-1]}
body = {'name': 'new_category'}
req = self._post('/catalog/categories', json.dumps(body))
result = req.get_response(self.api)
processed_result = json.loads(result.body)
self.assertIn('id', processed_result.keys())
expected['id'] = processed_result['id']
self.assertDictEqual(expected, processed_result)
def test_delete_category(self):
"""Check that category deleted successfully
"""
self._set_policy_rules({'delete_category': '@'})
self.expect_policy_check('delete_category',
{'category_id': '12345'})
fake_now = timeutils.utcnow()
expected = {'name': 'new_category',
'created': fake_now,
'updated': fake_now,
'id': '12345'}
e = models.Category(**expected)
test_utils.save_models(e)
req = self._delete('/catalog/categories/12345')
processed_result = req.get_response(self.api)
self.assertEqual('', processed_result.body)
self.assertEqual(200, processed_result.status_code)
def test_add_category_failed_for_non_admin(self):
"""Check that non admin user couldn't add new category
"""
self._set_policy_rules({'add_category': 'role:context_admin'})
self.is_admin = False
self.expect_policy_check('add_category')
fake_now = timeutils.utcnow()
timeutils.utcnow.override_time = fake_now
body = {'name': 'new_category'}
req = self._post('/catalog/categories', json.dumps(body))
result = req.get_response(self.api)
self.assertEqual(403, result.status_code)