Merge "Put, post operations implemented for environments"

This commit is contained in:
Jenkins 2016-08-16 08:15:38 +00:00 committed by Gerrit Code Review
commit 2903aa30a2
10 changed files with 484 additions and 175 deletions

View File

@ -11,6 +11,7 @@
# under the License.
LOG_LEVEL = 'DEBUG'
PROPAGATE_EXCEPTIONS = True
SQLALCHEMY_DATABASE_URI = \
'postgresql://tuningbox:tuningbox@localhost/tuningbox'

View File

@ -15,24 +15,33 @@ import itertools
import flask
import flask_restful
from flask_restful import fields
from sqlalchemy import exc as sa_exc
from werkzeug import exceptions
from tuning_box import converters
from tuning_box import db
from tuning_box import errors
from tuning_box.library import components
from tuning_box.library import environments
from tuning_box import logger
from tuning_box.middleware import keystone
# These handlers work if PROPAGATE_EXCEPTIONS is off (non-Nailgun case)
api_errors = {
'IntegrityError': {'status': 409}, # sqlalchemy IntegrityError
'TuningboxIntegrityError': {'status': 409},
'TuningboxNotFound': {'status': 404}
}
api = flask_restful.Api(errors=api_errors)
api.add_resource(components.ComponentsCollection, '/components')
api.add_resource(components.Component, '/components/<int:component_id>')
api.add_resource(environments.EnvironmentsCollection, '/environments')
api.add_resource(
environments.Environment,
'/environments/<int:environment_id>', # Backward compatibility support
'/environment/<int:environment_id>'
)
def with_transaction(f):
@ -44,55 +53,6 @@ def with_transaction(f):
return inner
environment_fields = {
'id': fields.Integer,
'components': fields.List(fields.Integer(attribute='id')),
'hierarchy_levels': fields.List(fields.String(attribute='name')),
}
@api.resource('/environments')
class EnvironmentsCollection(flask_restful.Resource):
method_decorators = [flask_restful.marshal_with(environment_fields)]
def get(self):
return db.Environment.query.all()
@with_transaction
def post(self):
component_ids = flask.request.json['components']
# TODO(yorik-sar): verify that resource names do not clash
components = [db.Component.query.get_by_id_or_name(i)
for i in component_ids]
hierarchy_levels = []
level = None
for name in flask.request.json['hierarchy_levels']:
level = db.EnvironmentHierarchyLevel(name=name, parent=level)
hierarchy_levels.append(level)
environment = db.Environment(components=components,
hierarchy_levels=hierarchy_levels)
if 'id' in flask.request.json:
environment.id = flask.request.json['id']
db.db.session.add(environment)
return environment, 201
@api.resource('/environments/<int:environment_id>')
class Environment(flask_restful.Resource):
method_decorators = [flask_restful.marshal_with(environment_fields)]
def get(self, environment_id):
return db.Environment.query.get_or_404(environment_id)
@with_transaction
def delete(self, environment_id):
environment = db.Environment.query.get_or_404(environment_id)
db.db.session.delete(environment)
return None, 204
def iter_environment_level_values(environment, levels):
env_levels = db.EnvironmentHierarchyLevel.get_for_environment(environment)
level_pairs = zip(env_levels, levels)
@ -250,6 +210,12 @@ def handle_integrity_error(exc):
return response
def handle_object_not_found(exc):
response = flask.jsonify(msg=exc.args[0])
response.status_code = 404
return response
def build_app(configure_logging=True, with_keystone=True):
app = flask.Flask(__name__)
app.url_map.converters.update(converters.ALL)
@ -259,6 +225,10 @@ def build_app(configure_logging=True, with_keystone=True):
app.config.from_envvar('TUNINGBOX_SETTINGS', silent=True)
# These handlers work if PROPAGATE_EXCEPTIONS is on (Nailgun case)
app.register_error_handler(sa_exc.IntegrityError, handle_integrity_error)
app.register_error_handler(errors.TuningboxIntegrityError,
handle_integrity_error)
app.register_error_handler(errors.TuningboxNotFound,
handle_object_not_found)
db.db.init_app(app)
if configure_logging:
log_level = app.config.get('LOG_LEVEL', 'INFO')

View File

@ -47,12 +47,12 @@ def fk(cls, **kwargs):
class BaseQuery(flask_sqlalchemy.BaseQuery):
def get_by_id_or_name(self, id_or_name):
def get_by_id_or_name(self, id_or_name, fail_on_none=True):
if isinstance(id_or_name, int):
result = self.get(id_or_name)
else:
result = self.filter_by(name=id_or_name).one_or_none()
if result is None:
if fail_on_none and result is None:
flask.abort(404)
return result

23
tuning_box/errors.py Normal file
View File

@ -0,0 +1,23 @@
# 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.
class BaseTuningboxError(Exception):
pass
class TuningboxIntegrityError(BaseTuningboxError):
pass
class TuningboxNotFound(BaseTuningboxError):
pass

View File

@ -0,0 +1,44 @@
# 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 tuning_box import errors
def load_objects(model, ids):
if ids is None:
return None
result = []
for obj_id in ids:
obj = model.query.filter_by(id=obj_id).one_or_none()
if obj is None:
raise errors.TuningboxNotFound(
"{0} not found by identifier: "
"{1}".format(model.__tablename__, obj_id)
)
result.append(obj)
return result
def load_objects_by_id_or_name(model, identifiers):
if identifiers is None:
return None
result = []
for identifier in identifiers:
obj = model.query.get_by_id_or_name(
identifier, fail_on_none=False)
if obj is None:
raise errors.TuningboxNotFound(
"{0} not found by identifier: "
"{1}".format(model.__tablename__, identifier)
)
result.append(obj)
return result

View File

@ -16,7 +16,7 @@ import flask_restful
from flask_restful import fields
from tuning_box import db
from tuning_box import library
resource_definition_fields = {
'id': fields.Integer,
@ -64,14 +64,8 @@ class Component(flask_restful.Resource):
component.name = update_by.get('name', component.name)
resource_definitions = update_by.get('resource_definitions')
if resource_definitions is not None:
resources = []
for resource_data in resource_definitions:
resource = db.ResourceDefinition.query.filter_by(
id=resource_data.get('id')
).one()
resource.component_id = component.id
db.db.session.add(resource)
resources.append(resource)
ids = [data['id'] for data in resource_definitions]
resources = library.load_objects(db.ResourceDefinition, ids)
component.resource_definitions = resources
def put(self, component_id):

View File

@ -0,0 +1,115 @@
# 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 flask
import flask_restful
from flask_restful import fields
from tuning_box import db
from tuning_box import errors
from tuning_box import library
environment_fields = {
'id': fields.Integer,
'components': fields.List(fields.Integer(attribute='id')),
'hierarchy_levels': fields.List(fields.String(attribute='name')),
}
class EnvironmentsCollection(flask_restful.Resource):
method_decorators = [flask_restful.marshal_with(environment_fields)]
def get(self):
return db.Environment.query.all()
def _check_components(self, components):
identities = set()
duplicates = set()
id_names = ('id', 'name')
for component in components:
for id_name in id_names:
value = getattr(component, id_name)
if value not in identities:
identities.add(value)
else:
duplicates.add(value)
if duplicates:
raise errors.TuningboxIntegrityError(
"Components duplicates: {0}".format(duplicates))
@db.with_transaction
def post(self):
component_ids = flask.request.json['components']
components = [db.Component.query.get_by_id_or_name(i)
for i in component_ids]
self._check_components(components)
hierarchy_levels = []
level = None
for name in flask.request.json['hierarchy_levels']:
level = db.EnvironmentHierarchyLevel(name=name, parent=level)
hierarchy_levels.append(level)
environment = db.Environment(components=components,
hierarchy_levels=hierarchy_levels)
if 'id' in flask.request.json:
environment.id = flask.request.json['id']
db.db.session.add(environment)
return environment, 201
class Environment(flask_restful.Resource):
method_decorators = [flask_restful.marshal_with(environment_fields)]
def get(self, environment_id):
return db.Environment.query.get_or_404(environment_id)
def _update_components(self, environment, components):
if components is not None:
new_components = library.load_objects_by_id_or_name(
db.Component, components)
environment.components = new_components
def _update_hierarchy_levels(self, environment, hierarchy_levels):
if hierarchy_levels is not None:
new_hierarchy_levels = library.load_objects_by_id_or_name(
db.EnvironmentHierarchyLevel, hierarchy_levels)
parent = None
for level in new_hierarchy_levels:
level.parent = parent
parent = level
environment.hierarchy_levels = new_hierarchy_levels
@db.with_transaction
def _perform_update(self, environment_id):
environment = db.Environment.query.get_or_404(environment_id)
update_by = flask.request.json
components = update_by.get('components')
self._update_components(environment, components)
hierarchy_levels = update_by.get('hierarchy_levels')
self._update_hierarchy_levels(environment, hierarchy_levels)
def put(self, environment_id):
return self.patch(environment_id)
def patch(self, env_id):
self._perform_update(env_id)
return None, 204
@db.with_transaction
def delete(self, environment_id):
environment = db.Environment.query.get_or_404(environment_id)
db.db.session.delete(environment)
return None, 204

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from tuning_box import db
from tuning_box.library import components
@ -156,6 +157,21 @@ class TestComponents(BaseTest):
self.assertItemsEqual(initial_data['resource_definitions'],
actual_component['resource_definitions'])
def test_put_component_resource_not_found(self):
self._fixture()
component_url = '/components/7'
initial_data = self._component_json
resource_definition = copy.deepcopy(
initial_data['resource_definitions'][0])
resource_definition['id'] = None
res = self.client.put(
component_url,
data={'resource_definitions': [resource_definition]}
)
self.assertEqual(404, res.status_code)
def test_put_component_ignore_changing_id(self):
self._fixture()
component_url = '/components/7'

View File

@ -0,0 +1,260 @@
# 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 tuning_box import db
from tuning_box import library
from tuning_box.library import environments
from tuning_box.tests.test_app import BaseTest
class TestEnvironments(BaseTest):
def test_get_environments_empty(self):
res = self.client.get('/environments')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.json, [])
def test_get_environments(self):
self._fixture()
res = self.client.get('/environments')
self.assertEqual(200, res.status_code)
self.assertEqual(1, len(res.json))
self.assertItemsEqual(
{'id': 9, 'components': [7], 'hierarchy_levels': ['lvl1', 'lvl2']},
res.json[0]
)
def test_get_one_environment(self):
self._fixture()
res = self.client.get('/environments/9')
self.assertEqual(200, res.status_code)
self.assertItemsEqual(
{'id': 9, 'components': [7], 'hierarchy_levels': ['lvl1', 'lvl2']},
res.json
)
def test_get_one_environment_404(self):
res = self.client.get('/environments/9')
self.assertEqual(res.status_code, 404)
def test_post_environment(self):
self._fixture()
json = {'components': [7], 'hierarchy_levels': ['lvla', 'lvlb']}
res = self.client.post('/environments', data=json)
self.assertEqual(res.status_code, 201)
json['id'] = res.json['id']
self.assertItemsEqual(json, res.json)
self._assert_db_effect(
db.Environment, res.json['id'],
environments.environment_fields, json)
def test_post_environment_preserve_id(self):
self._fixture()
json = {
'id': 42,
'components': [7],
'hierarchy_levels': ['lvla', 'lvlb'],
}
res = self.client.post('/environments', data=json)
self.assertEqual(201, res.status_code)
self.assertItemsEqual(json, res.json)
self._assert_db_effect(
db.Environment, 42, environments.environment_fields, json)
def test_post_environment_preserve_id_conflict(self):
self._fixture()
json = {
'id': 9,
'components': [7],
'hierarchy_levels': ['lvla', 'lvlb'],
}
res = self.client.post('/environments', data=json)
self.assertEqual(res.status_code, 409)
def test_post_environment_preserve_id_conflict_propagate_exc(self):
self.app.config["PROPAGATE_EXCEPTIONS"] = True
self._fixture()
json = {
'id': 9,
'components': [7],
'hierarchy_levels': ['lvla', 'lvlb'],
}
res = self.client.post('/environments', data=json)
self.assertEqual(res.status_code, 409)
def test_post_environment_by_component_name(self):
self._fixture()
json = {
'components': ['component1'],
'hierarchy_levels': ['lvla', 'lvlb'],
}
res = self.client.post('/environments', data=json)
self.assertEqual(res.status_code, 201)
json['id'] = res.json['id']
json['components'] = [7]
self.assertItemsEqual(json, res.json)
self._assert_db_effect(
db.Environment, res.json['id'],
environments.environment_fields, json)
def test_post_components_duplication(self):
self._fixture()
json = {
'components': ['component1', 7],
'hierarchy_levels': ['lvl'],
}
res = self.client.post('/environments', data=json)
self.assertEqual(409, res.status_code)
def test_post_components_no_duplication(self):
self._fixture()
components_url = '/components'
res = self.client.get(components_url)
self.assertEqual(200, res.status_code)
component = res.json[0]
# Creating component with name equal to id of existed component
res = self.client.post(
components_url,
data={
'name': component['id'],
'resource_definitions': []
}
)
self.assertEqual(201, res.status_code)
new_component = res.json
# Checking no components duplication detected
json = {
'components': [component['id'], new_component['name']],
'hierarchy_levels': ['lvl'],
}
res = self.client.post('/environments', data=json)
self.assertEqual(201, res.status_code)
def test_post_environment_404(self):
self._fixture()
json = {'components': [8], 'hierarchy_levels': ['lvla', 'lvlb']}
res = self.client.post('/environments', data=json)
self.assertEqual(res.status_code, 404)
self._assert_not_in_db(db.Environment, 10)
def test_post_environment_by_component_name_404(self):
self._fixture()
json = {
'components': ['component2'],
'hierarchy_levels': ['lvla', 'lvlb'],
}
res = self.client.post('/environments', data=json)
self.assertEqual(res.status_code, 404)
self._assert_not_in_db(db.Environment, 10)
def test_delete_environment(self):
self._fixture()
res = self.client.delete('/environments/9')
self.assertEqual(res.status_code, 204)
self.assertEqual(res.data, b'')
self._assert_not_in_db(db.Environment, 9)
def test_delete_environment_404(self):
res = self.client.delete('/environments/9')
self.assertEqual(res.status_code, 404)
def test_put_environment_404(self):
res = self.client.put('/environments/7')
self.assertEqual(res.status_code, 404)
def test_put_environment_components(self):
self._fixture()
environment_url = '/environment/9'
initial = self.client.get(environment_url).json
# Updating components
res = self.client.put(environment_url,
data={'components': []})
self.assertEqual(204, res.status_code)
actual = self.client.get(environment_url).json
self.assertEqual([], actual['components'])
# Restoring components
res = self.client.put(
environment_url,
data={'components': initial['components']}
)
self.assertEqual(204, res.status_code)
actual = self.client.get(environment_url).json
self.assertItemsEqual(initial, actual)
def test_put_environment_component_not_found(self):
self._fixture()
environment_url = '/environment/9'
res = self.client.put(
environment_url,
data={'components': [None]}
)
self.assertEqual(404, res.status_code)
def check_hierarchy_levels(self, hierarchy_levels_names):
with self.app.app_context():
hierarchy_levels = library.load_objects_by_id_or_name(
db.EnvironmentHierarchyLevel, hierarchy_levels_names)
parent_id = None
for level in hierarchy_levels:
self.assertEqual(parent_id, level.parent_id)
parent_id = level.id
def test_put_environment_hierarchy_levels(self):
self._fixture()
environment_url = '/environment/9'
initial = self.client.get(environment_url).json
# Updating hierarchy levels
res = self.client.put(environment_url,
data={'hierarchy_levels': []})
self.assertEqual(204, res.status_code)
actual = self.client.get(environment_url).json
self.assertEqual([], actual['hierarchy_levels'])
# Restoring levels
res = self.client.put(
environment_url,
data={'hierarchy_levels': initial['hierarchy_levels']}
)
self.assertEqual(204, res.status_code)
actual = self.client.get(environment_url).json
self.assertItemsEqual(initial, actual)
self.check_hierarchy_levels(actual['hierarchy_levels'])
def test_put_environment_hierarchy_levels_remove_level(self):
self._fixture()
environment_url = '/environment/9'
initial = self.client.get(environment_url).json
expected_levels = initial['hierarchy_levels'][1:]
# Updating hierarchy levels
res = self.client.put(
environment_url,
data={'hierarchy_levels': expected_levels}
)
self.assertEqual(204, res.status_code)
actual = self.client.get(environment_url).json
self.assertEqual(expected_levels, actual['hierarchy_levels'])
self.check_hierarchy_levels(actual['hierarchy_levels'])
def test_put_environment_level_not_found(self):
self._fixture()
environment_url = '/environment/9'
res = self.client.put(
environment_url,
data={'hierarchy_levels': [None]}
)
self.assertEqual(404, res.status_code)

View File

@ -88,120 +88,6 @@ class BaseTest(base.TestCase):
class TestApp(BaseTest):
def test_get_environments_empty(self):
res = self.client.get('/environments')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.json, [])
def test_get_environments(self):
self._fixture()
res = self.client.get('/environments')
self.assertEqual(200, res.status_code)
self.assertEqual(1, len(res.json))
self.assertItemsEqual(
{'id': 9, 'components': [7], 'hierarchy_levels': ['lvl1', 'lvl2']},
res.json[0]
)
def test_get_one_environment(self):
self._fixture()
res = self.client.get('/environments/9')
self.assertEqual(200, res.status_code)
self.assertItemsEqual(
{'id': 9, 'components': [7], 'hierarchy_levels': ['lvl1', 'lvl2']},
res.json
)
def test_get_one_environment_404(self):
res = self.client.get('/environments/9')
self.assertEqual(res.status_code, 404)
def test_post_environment(self):
self._fixture()
json = {'components': [7], 'hierarchy_levels': ['lvla', 'lvlb']}
res = self.client.post('/environments', data=json)
self.assertEqual(res.status_code, 201)
json['id'] = res.json['id']
self.assertItemsEqual(json, res.json)
self._assert_db_effect(
db.Environment, res.json['id'], app.environment_fields, json)
def test_post_environment_preserve_id(self):
self._fixture()
json = {
'id': 42,
'components': [7],
'hierarchy_levels': ['lvla', 'lvlb'],
}
res = self.client.post('/environments', data=json)
self.assertEqual(201, res.status_code)
self.assertItemsEqual(json, res.json)
self._assert_db_effect(
db.Environment, 42, app.environment_fields, json)
def test_post_environment_preserve_id_conflict(self):
self._fixture()
json = {
'id': 9,
'components': [7],
'hierarchy_levels': ['lvla', 'lvlb'],
}
res = self.client.post('/environments', data=json)
self.assertEqual(res.status_code, 409)
def test_post_environment_preserve_id_conflict_propagate_exc(self):
self.app.config["PROPAGATE_EXCEPTIONS"] = True
self._fixture()
json = {
'id': 9,
'components': [7],
'hierarchy_levels': ['lvla', 'lvlb'],
}
res = self.client.post('/environments', data=json)
self.assertEqual(res.status_code, 409)
def test_post_environment_by_component_name(self):
self._fixture()
json = {
'components': ['component1'],
'hierarchy_levels': ['lvla', 'lvlb'],
}
res = self.client.post('/environments', data=json)
self.assertEqual(res.status_code, 201)
json['id'] = res.json['id']
json['components'] = [7]
self.assertItemsEqual(json, res.json)
self._assert_db_effect(
db.Environment, res.json['id'], app.environment_fields, json)
def test_post_environment_404(self):
self._fixture()
json = {'components': [8], 'hierarchy_levels': ['lvla', 'lvlb']}
res = self.client.post('/environments', data=json)
self.assertEqual(res.status_code, 404)
self._assert_not_in_db(db.Environment, 10)
def test_post_environment_by_component_name_404(self):
self._fixture()
json = {
'components': ['component2'],
'hierarchy_levels': ['lvla', 'lvlb'],
}
res = self.client.post('/environments', data=json)
self.assertEqual(res.status_code, 404)
self._assert_not_in_db(db.Environment, 10)
def test_delete_environment(self):
self._fixture()
res = self.client.delete('/environments/9')
self.assertEqual(res.status_code, 204)
self.assertEqual(res.data, b'')
self._assert_not_in_db(db.Environment, 9)
def test_delete_environment_404(self):
res = self.client.delete('/environments/9')
self.assertEqual(res.status_code, 404)
def test_get_environment_level_value_root(self):
self._fixture()
with self.app.app_context(), db.db.session.begin():