Skip to content
Snippets Groups Projects
Commit 817d7cd6 authored by Nikolay Stanchev's avatar Nikolay Stanchev
Browse files

CLMC service clean up of experimental configuration API

parent 5e1bc0be
No related branches found
No related tags found
No related merge requests found
#!/usr/bin/python3
"""
// © University of Southampton IT Innovation Centre, 2018
//
// Copyright in this software belongs to University of Southampton
// IT Innovation Centre of Gamma House, Enterprise Road,
// Chilworth Science Park, Southampton, SO16 7NS, UK.
//
// This software may not be used, sold, licensed, transferred, copied
// or reproduced in whole or in part in any manner or form or in or
// on any media by any person other than in accordance with the terms
// of the Licence Agreement supplied with the software, or otherwise
// without the prior written consent of the copyright owners.
//
// This software is distributed WITHOUT ANY WARRANTY, without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE, except where stated in the Licence Agreement supplied with
// the software.
//
// Created By : Nikolay Stanchev
// Created Date : 03-07-2018
// Created for Project : FLAME
"""
import pytest
from clmcservice.whoamiapi.conftest import create_test_database, initialise_database, drop_test_database
@pytest.fixture(scope='module', autouse=True)
def testing_db_session():
test_database = "configtestdb"
create_test_database(test_database) # create a database used for executing the unit tests
db_session, engine = initialise_database(test_database) # initialise the database with the models and retrieve a db session
yield db_session # return the db session if needed in any of the tests
db_session.remove() # remove the db session
engine.dispose() # dispose from the engine
drop_test_database(test_database) # remove the test database
#!/usr/bin/python3
"""
// © University of Southampton IT Innovation Centre, 2018
//
// Copyright in this software belongs to University of Southampton
// IT Innovation Centre of Gamma House, Enterprise Road,
// Chilworth Science Park, Southampton, SO16 7NS, UK.
//
// This software may not be used, sold, licensed, transferred, copied
// or reproduced in whole or in part in any manner or form or in or
// on any media by any person other than in accordance with the terms
// of the Licence Agreement supplied with the software, or otherwise
// without the prior written consent of the copyright owners.
//
// This software is distributed WITHOUT ANY WARRANTY, without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE, except where stated in the Licence Agreement supplied with
// the software.
//
// Created By : Nikolay Stanchev
// Created Date : 02-07-2018
// Created for Project : FLAME
"""
import pytest
from json import dumps
from pyramid import testing
from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPConflict
from clmcservice.models import ServiceFunctionChain
from clmcservice.configapi.views import SFCConfigAPI
class TestSFCConfigAPI(object):
"""
A pytest-implementation test for the Config API endpoints for service function chains
"""
@pytest.fixture(autouse=True)
def app_config(self):
"""
A fixture to implement setUp/tearDown functionality for all tests by initializing configuration structure for the web service and db connection
"""
self.registry = testing.setUp()
yield
testing.tearDown()
ServiceFunctionChain.delete_all() # clear the instances of the model in the test database
def test_get_all(self):
"""
Tests the GET all method of the config API for service function chains - returns a list of all service function chains from the database.
"""
request = testing.DummyRequest()
response = SFCConfigAPI(request).get_all()
assert response == [], "Initially there mustn't be any service function chains in the database."
sfc = ServiceFunctionChain(sfc="sfc1", chain={"nginx": ["minio"]})
expected_response_data = [sfc.json]
ServiceFunctionChain.add(sfc) # adds the new instance of the model to the database
request = testing.DummyRequest()
response = SFCConfigAPI(request).get_all()
assert response == expected_response_data, "Incorrect response data with 1 service function chain."
sfc = ServiceFunctionChain(sfc="sfc2", chain={"nginx": ["minio"]})
expected_response_data.append(sfc.json)
ServiceFunctionChain.add(sfc)
sfc = ServiceFunctionChain(sfc="sfc3", chain={"nginx": ["minio"]})
expected_response_data.append(sfc.json)
ServiceFunctionChain.add(sfc)
request = testing.DummyRequest()
response = SFCConfigAPI(request).get_all()
assert response == expected_response_data, "Incorrect response data with more than 1 service function chains."
def test_get_one(self):
"""
Tests the GET one method of the config API for service function chains - returns a service function chain from the database.
"""
request = testing.DummyRequest()
response = SFCConfigAPI(request).get_all()
assert response == [], "Initially there mustn't be any service function chains in the database."
self._validation_of_url_parameters_test("get_one")
sfc = ServiceFunctionChain(sfc="sfc1", chain={"nginx": ["minio"]})
expected_response_data = sfc.json
ServiceFunctionChain.add(sfc) # adds the new instance of the model to the database
request = testing.DummyRequest()
request.params["sfc"] = "sfc1"
response = SFCConfigAPI(request).get_one()
assert response == expected_response_data, "Invalid data returned in the response of GET instance"
request = testing.DummyRequest()
request.params["sfc"] = "sfc2"
error_raised = False
try:
SFCConfigAPI(request).get_one()
except HTTPNotFound:
error_raised = True
assert error_raised, "Not found error must be raised in case of a non existing service function chain"
def test_post(self):
"""
Tests the POST method of the config API for service function chains - creates a service function chain in the database.
"""
request = testing.DummyRequest()
response = SFCConfigAPI(request).get_all()
assert response == [], "Initially there mustn't be any service function chains in the database."
resource = dict(sfc="sfc1", chain={"nginx": ["minio"]})
json_data = dumps(resource)
request = testing.DummyRequest()
request.body = json_data.encode(request.charset)
response = SFCConfigAPI(request).post()
assert response == resource, "POST request must return the created resource"
assert ServiceFunctionChain.exists("sfc1"), "POST request must have created the resource"
resource["chain"] = {}
json_data = dumps(resource)
request = testing.DummyRequest()
request.body = json_data.encode(request.charset)
error_raised = False
try:
SFCConfigAPI(request).post()
except HTTPConflict:
error_raised = True
assert error_raised, "An error must be raised when trying to create a resource which breaks the unique constraint"
@pytest.mark.parametrize("body, valid", [
('{"sfc": "sfc1", "chain":{"nginx":["minio"]}}', True),
('{"sfc": "sfc2", "chain":{}}', True),
('{"sfc": "sfc1", "chain":[]}', False),
('{}', False),
('{"sfc": "sfc3"}', False),
('{"sf": "sfc2", "sf_i": "sfc_i2", "chain":{}', False),
('{invalid json}', False),
])
def test_post_body_validation(self, body, valid):
"""
Tests the POST request validation of the body content.
:param body: The request body to be validated
:param valid: True if body is valid, False otherwise
"""
request = testing.DummyRequest()
request.body = body.encode(request.charset)
error_raised = False
try:
SFCConfigAPI(request).post()
except HTTPBadRequest:
error_raised = True
assert error_raised == (not valid), "An error must be raised in case of an invalid request body"
def test_put(self):
"""
Tests the PUT method of the Config API for service function chains - overwrites a service function chain from the database.
"""
request = testing.DummyRequest()
response = SFCConfigAPI(request).get_all()
assert response == [], "Initially there mustn't be any service function chains in the database."
self._validation_of_url_parameters_test("put")
resource = dict(sfc="sfc1", chain={"nginx": ["minio"]})
body = dumps(resource)
request = testing.DummyRequest()
request.params["sfc"] = "sfc1"
request.body = body.encode(request.charset)
error_raised = False
try:
SFCConfigAPI(request).put()
except HTTPNotFound:
error_raised = True
assert error_raised, "Not found error must be raised in case of a non existing service function chain"
sfc = ServiceFunctionChain(sfc="sfc1", chain={"nginx": ["minio"]})
ServiceFunctionChain.add(sfc) # adds the new instance of the model to the database
resource = dict(sfc="sfc1", chain={})
body = dumps(resource)
request = testing.DummyRequest()
request.params["sfc"] = "sfc1"
request.body = body.encode(request.charset)
response = SFCConfigAPI(request).put()
assert response == resource, "PUT request must return the updated resource"
assert ServiceFunctionChain.get("sfc1").json["chain"] == {}
resource = dict(sfc="sfc2", chain={"nginx": ["minio"]})
body = dumps(resource)
request = testing.DummyRequest()
request.params["sfc"] = "sfc1"
request.body = body.encode(request.charset)
response = SFCConfigAPI(request).put()
assert response == resource, "PUT request must return the updated resource"
assert not ServiceFunctionChain.exists("sfc1"), "Resource has not been updated"
assert ServiceFunctionChain.exists("sfc2"), "Resource has not been updated"
sfc = ServiceFunctionChain(sfc="sfc1", chain={"nginx": ["minio"]})
ServiceFunctionChain.add(sfc) # adds the new instance of the model to the database
resource = dict(sfc="sfc2", chain={"nginx": ["minio"]})
body = dumps(resource)
request = testing.DummyRequest()
request.params["sfc"] = "sfc1"
request.body = body.encode(request.charset)
error_raised = False
try:
SFCConfigAPI(request).put()
except HTTPConflict:
error_raised = True
assert error_raised, "PUT request breaks unique constraint"
@pytest.mark.parametrize("body, valid", [
('{"sfc": "sfc1", "chain":{"nginx":["minio"]}}', True),
('{"sfc": "sfc2", "chain":{}}', True),
('{"sfc": "sfc1", "chain":[]}', False),
('{}', False),
('{"sfc": "sfc3"}', False),
('{"sf": "sfc2", "sf_i": "sfc_i2", "chain":{}', False),
('{invalid json}', False),
])
def test_put_body_validation(self, body, valid):
"""
Tests the PUT request validation of the body content.
:param body: The request body to be validated
:param valid: True if body is valid, False otherwise
"""
sfc = ServiceFunctionChain(sfc="sfc1", chain={"nginx": ["minio"]})
ServiceFunctionChain.add(sfc) # adds the new instance of the model to the database
request = testing.DummyRequest()
request.params["sfc"] = "sfc1"
request.body = body.encode(request.charset)
error_raised = False
try:
SFCConfigAPI(request).put()
except HTTPBadRequest:
error_raised = True
assert error_raised == (not valid), "An error must be raised in case of an invalid request body"
def test_delete(self):
"""
Tests the DELETE method of the config API for service function chains - deletes a service function chain from the database.
"""
request = testing.DummyRequest()
response = SFCConfigAPI(request).get_all()
assert response == [], "Initially there mustn't be any service function chains in the database."
self._validation_of_url_parameters_test("delete")
sfc = ServiceFunctionChain(sfc="sfc1", chain={"nginx": ["minio"]})
to_delete = sfc.json
ServiceFunctionChain.add(sfc) # adds the new instance of the model to the database
assert ServiceFunctionChain.exists("sfc1")
request = testing.DummyRequest()
request.params["sfc"] = "sfc1"
response = SFCConfigAPI(request).delete()
assert response == to_delete, "DELETE must return the deleted object if successful"
assert not ServiceFunctionChain.exists("sfc1"), "Resource must be deleted after the delete API method has been called."
request = testing.DummyRequest()
request.params["sfc"] = "sfc1"
error_raised = False
try:
SFCConfigAPI(request).delete()
except HTTPNotFound:
error_raised = True
assert error_raised, "Not found error must be raised in case of a non existing service function chain"
@staticmethod
def _validation_of_url_parameters_test(method):
"""
Validates the way a config API method handles url query parameters for service function chains
:param method: the method to test
"""
request = testing.DummyRequest()
error_raised = False
try:
getattr(SFCConfigAPI(request), method).__call__()
except HTTPBadRequest:
error_raised = True
assert error_raised, "Error must be raised in case of no URL parameters"
request = testing.DummyRequest()
request.params["sfc_i"] = "sfc1" # argument should be sfc
try:
getattr(SFCConfigAPI(request), method).__call__()
except HTTPBadRequest:
error_raised = True
assert error_raised, "Error must be raised in case of insufficient number of arguments"
request = testing.DummyRequest()
request.params["sf"] = "sfc1" # argument should be sfc
try:
getattr(SFCConfigAPI(request), method).__call__()
except HTTPBadRequest:
error_raised = True
assert error_raised, "Error must be raised in case of invalid naming of arguments"
#!/usr/bin/python3
"""
// © University of Southampton IT Innovation Centre, 2018
//
// Copyright in this software belongs to University of Southampton
// IT Innovation Centre of Gamma House, Enterprise Road,
// Chilworth Science Park, Southampton, SO16 7NS, UK.
//
// This software may not be used, sold, licensed, transferred, copied
// or reproduced in whole or in part in any manner or form or in or
// on any media by any person other than in accordance with the terms
// of the Licence Agreement supplied with the software, or otherwise
// without the prior written consent of the copyright owners.
//
// This software is distributed WITHOUT ANY WARRANTY, without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE, except where stated in the Licence Agreement supplied with
// the software.
//
// Created By : Nikolay Stanchev
// Created Date : 02-07-2018
// Created for Project : FLAME
"""
from json import loads
from clmcservice.models import ServiceFunctionChain
def validate_sfchain_body(body):
"""
Validates the request body used to create an service function chain resource in the database.
:param body: the request body to validate
:return the validated sfc dictionary object
:raise AssertionError: if the body is not a valid service function chain
"""
try:
body = loads(body)
except:
raise AssertionError("Service function chain must be represented by a JSON object.")
assert len(body) == len(ServiceFunctionChain.__table__.columns), "Service function chain JSON object mustn't contain a different number of attributes than the number of required ones."
# validate that all required attributes are given in the body
for attribute in ServiceFunctionChain.__table__.columns:
assert attribute.name in body, "Required attribute not found in the request content."
assert type(body["chain"]) == dict, "The chain attribute of a service function chain must be a graph representing the relations between service functions."
for sf in body["chain"]:
assert type(body["chain"][sf]) == list, "A list must be used to represent each dependency between service functions"
return body
#!/usr/bin/python3
"""
// © University of Southampton IT Innovation Centre, 2018
//
// Copyright in this software belongs to University of Southampton
// IT Innovation Centre of Gamma House, Enterprise Road,
// Chilworth Science Park, Southampton, SO16 7NS, UK.
//
// This software may not be used, sold, licensed, transferred, copied
// or reproduced in whole or in part in any manner or form or in or
// on any media by any person other than in accordance with the terms
// of the Licence Agreement supplied with the software, or otherwise
// without the prior written consent of the copyright owners.
//
// This software is distributed WITHOUT ANY WARRANTY, without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE, except where stated in the Licence Agreement supplied with
// the software.
//
// Created By : Nikolay Stanchev
// Created Date : 02-07-2018
// Created for Project : FLAME
"""
from pyramid.httpexceptions import HTTPBadRequest, HTTPConflict, HTTPNotFound
from pyramid.view import view_defaults, view_config
from clmcservice.models import ServiceFunctionChain
from clmcservice.configapi.utilities import validate_sfchain_body
@view_defaults(renderer='json')
class SFCConfigAPI(object):
"""
A class-based view for posting and retrieving configuration data for service function chains to the CLMC service
"""
def __init__(self, request):
"""
Initialises the instance of the view with the request argument.
:param request: client's call request
"""
self.request = request
@view_config(route_name='config_sfc', request_method='GET')
def get_all(self):
"""
GET API call for all resources.
:return: A list of all service function chains found in the database.
"""
return [instance.json for instance in ServiceFunctionChain.query()]
@view_config(route_name='config_sfc_instance', request_method='GET')
def get_one(self):
"""
GET API call for a single resources.
:return: One service function chain instance retrieved from the database by querying the sfc ID
:raises HTTPBadRequest: if the request parameters are invalid(invalid url query string)
:raises HTTPNotFound: if a resource with the given parameters doesn't exist in the database
"""
sf_chain = self._get_sf_chain_from_url_string()
if sf_chain is None:
raise HTTPNotFound("A service function chain with the given parameters doesn't exist.")
else:
return sf_chain.json
@view_config(route_name='config_sfc', request_method='POST')
def post(self):
"""
A POST API call to create a new service function chain.
:return: A JSON response to the POST call - essentially with the data of the new resource
:raises HTTPBadRequest: if request body is not a valid JSON for the service function chain
:raises HTTPConflict: if the unique constraints are not preserved after the creation of a new instance
"""
# create an instance of the model and add it to the database table
sf_chain = self._validate_and_create()
json_data = sf_chain.json
ServiceFunctionChain.add(sf_chain)
self.request.response.status = 201
return json_data
@view_config(route_name='config_sfc_instance', request_method='PUT')
def put(self):
"""
A PUT API call to update a service function chain.
:return: A JSON response representing the updated object
:raises HTTPBadRequest: if the request parameters are invalid(invalid url query string)
:raises HTTPNotFound: if a resource with the given parameters doesn't exist in the database
"""
sf_chain = self._get_sf_chain_from_url_string()
if sf_chain is None:
raise HTTPNotFound("A service function chain with the given ID doesn't exist.")
else:
try:
body = self.request.body.decode(self.request.charset)
validated_body = validate_sfchain_body(body) # validate the content and receive a json dictionary object
except AssertionError as e:
raise HTTPBadRequest("Bad request content. Service function chain format is incorrect: {0}".format(e.args))
new_resource = validated_body
old_resource = sf_chain.json
updating = new_resource["sfc"] == old_resource["sfc"]
if updating:
ServiceFunctionChain.delete(sf_chain)
new_sf_chain = ServiceFunctionChain(**validated_body)
ServiceFunctionChain.add(new_sf_chain)
else:
resource_exists = ServiceFunctionChain.exists(new_resource["sfc"])
if resource_exists:
raise HTTPConflict("Service function chain with this data already exists.") # error 409 in case of resource conflict
new_sf_chain = ServiceFunctionChain(**validated_body)
ServiceFunctionChain.replace(sf_chain, new_sf_chain)
return validated_body
@view_config(route_name='config_sfc_instance', request_method='DELETE')
def delete(self):
"""
Deletes an instance of a service function chain in the database.
:return: An content of the object that has been deleted
:raises HTTPBadRequest: if the request parameters are invalid(invalid url query string)
:raises HTTPNotFound: if a resource with the given parameters doesn't exist in the database
"""
sf_chain = self._get_sf_chain_from_url_string()
if sf_chain is None:
raise HTTPNotFound("A service function chain with the given ID doesn't exist.")
else:
deleted = sf_chain.json
ServiceFunctionChain.delete(sf_chain)
return deleted
def _get_sf_chain_from_url_string(self):
"""
Retrieves a service function chain from the database by validating and then using the request url parameters.
:return: An instance of a service function chain or None if not existing
"""
if "sfc" not in self.request.params:
raise HTTPBadRequest("Request format is incorrect: URL argument 'sfc' not found")
sf_chain = ServiceFunctionChain.get(sfc=self.request.params["sfc"])
return sf_chain
def _validate_and_create(self):
"""
Validates the request body and checks if a resource with the given attributes already exists.
:return: a new instance of the model, if the resource doesn't exist
:raises HTTPBadRequest: if request body is not a valid JSON for the service function chain
:raises HTTPConflict: if the unique constraints are not preserved after the creation of a new instance
"""
try:
body = self.request.body.decode(self.request.charset)
validated_body = validate_sfchain_body(body) # validate the content and receive a json dictionary object
except AssertionError as e:
raise HTTPBadRequest("Bad request content. Service function chain format is incorrect: {0}".format(e.args))
resource = validated_body
resource_exists = ServiceFunctionChain.exists(resource["sfc"])
if resource_exists:
raise HTTPConflict("Service function chain with this data already exists.") # error 409 in case of resource conflict
# create an instance of the model
sf_chain = ServiceFunctionChain(**resource)
return sf_chain
from .meta import DBSession
from .whoami_models import ServiceFunctionEndpoint
from .config_models import ServiceFunctionChain
#!/usr/bin/python3
"""
// © University of Southampton IT Innovation Centre, 2018
//
// Copyright in this software belongs to University of Southampton
// IT Innovation Centre of Gamma House, Enterprise Road,
// Chilworth Science Park, Southampton, SO16 7NS, UK.
//
// This software may not be used, sold, licensed, transferred, copied
// or reproduced in whole or in part in any manner or form or in or
// on any media by any person other than in accordance with the terms
// of the Licence Agreement supplied with the software, or otherwise
// without the prior written consent of the copyright owners.
//
// This software is distributed WITHOUT ANY WARRANTY, without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE, except where stated in the Licence Agreement supplied with
// the software.
//
// Created By : Nikolay Stanchev
// Created Date : 02-07-2018
// Created for Project : FLAME
"""
from sqlalchemy import Column, String, and_
from sqlalchemy.dialects.postgresql import JSONB
from clmcservice.models.meta import Base
class ServiceFunctionChain(Base):
"""
This class defines the service function chain model of the config API, declaring the relations between individual service functions per service function chain.
"""
__tablename__ = 'sfchain' # table name in the PostgreSQL database
sfc = Column(String, nullable=False, primary_key=True) # service function chain label
chain = Column(JSONB, nullable=False) # the service function chain graph represented by a python dictionary (JSON object essentially)
@property
def json(self):
"""
Converts an instance of a ServiceFunctionChain to JSON format.
:return: a python dictionary object
"""
fields = {c.name: getattr(self, c.name) for c in self.__table__.columns}
return fields
@staticmethod
def required_columns():
"""
Returns the required columns for constructing a valid instance.
:return: a generator object
"""
return tuple(column.name for column in ServiceFunctionChain.__table__.columns)
@staticmethod
def get(sfc):
"""
Gets the instance matching the sfc argument
:param sfc: service function chain id
:return: the first object from the result set that matches the sfc argument (must be only one)
"""
return ServiceFunctionChain.query().filter(and_(ServiceFunctionChain.sfc == sfc)).first()
@staticmethod
def exists(sfc):
"""
Checks if an instance matching the sfc exists.
:param sfc: service function chain id
:return: True if exists, False otherwise
"""
return ServiceFunctionChain.get(sfc) is not None
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment