From 00e90b39975c7c5e30720b7a0fe8f49d5a0eceac Mon Sep 17 00:00:00 2001 From: Nikolay Stanchev <ns17@it-innovation.soton.ac.uk> Date: Tue, 26 Jun 2018 19:23:49 +0100 Subject: [PATCH] WHOAMI API - initial implementation --- src/service/clmcservice/__init__.py | 5 + .../clmcservice/aggregationapi/__init__.py | 2 +- src/service/clmcservice/models.py | 152 +++++++++- src/service/clmcservice/whoamiapi/conftest.py | 89 ++++++ src/service/clmcservice/whoamiapi/tests.py | 278 ++++++++++++++++++ .../clmcservice/whoamiapi/utilities.py | 71 +++++ src/service/clmcservice/whoamiapi/views.py | 191 ++++++++++++ 7 files changed, 776 insertions(+), 12 deletions(-) create mode 100644 src/service/clmcservice/whoamiapi/conftest.py create mode 100644 src/service/clmcservice/whoamiapi/utilities.py diff --git a/src/service/clmcservice/__init__.py b/src/service/clmcservice/__init__.py index 69ee81b..d3cbc29 100644 --- a/src/service/clmcservice/__init__.py +++ b/src/service/clmcservice/__init__.py @@ -46,9 +46,14 @@ def main(global_config, **settings): config = Configurator(settings=settings) + # add routes of the aggregator API config.add_route('aggregator_config', '/aggregator/config') config.add_route('aggregator_controller', '/aggregator/control') config.add_route('round_trip_time_query', '/query/round-trip-time') + # add routes of the WHOAMI API + config.add_route('whoami_endpoints', '/whoami/endpoints') + config.add_route('whoami_endpoints_instance', 'whoami/endpoints/instance') + config.scan() # This method scans the packages and finds any views related to the routes added in the app configuration return config.make_wsgi_app() diff --git a/src/service/clmcservice/aggregationapi/__init__.py b/src/service/clmcservice/aggregationapi/__init__.py index 1bbd927..81bb249 100644 --- a/src/service/clmcservice/aggregationapi/__init__.py +++ b/src/service/clmcservice/aggregationapi/__init__.py @@ -1 +1 @@ -__all__ = ['utilities', 'views'] +__all__ = ['views'] diff --git a/src/service/clmcservice/models.py b/src/service/clmcservice/models.py index 840fc08..78085d8 100644 --- a/src/service/clmcservice/models.py +++ b/src/service/clmcservice/models.py @@ -1,11 +1,85 @@ +import transaction from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker from zope.sqlalchemy import ZopeTransactionExtension -from sqlalchemy import Column, String, Integer, UniqueConstraint - +from sqlalchemy import Column, String, Integer, UniqueConstraint, and_ DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension())) # initialise a ORM session, ought to be reused across the different modules -Base = declarative_base() # initialise a declarative Base instance to use for the web app models + + +class ORMClass(object): + """ + Declares a parent class for all models which eases querying + """ + + @classmethod + def query(cls): + """ + Pass down the class name when using the DBSession.query method and use ModelClass.query() instead of DBSession.query(ModelClass) + + :return: the query result object + """ + + global DBSession + + return DBSession.query(cls) + + @staticmethod + def add(instance): + """ + Adds an instance of a model to the database. + + :param instance: the instance to be created in the db. + """ + + global DBSession + + with transaction.manager: + DBSession.add(instance) + + @staticmethod + def delete(instance): + """ + Deletes an instance of a model from the database. + + :param instance: the instance to be deleted from the db. + """ + + global DBSession + + with transaction.manager: + DBSession.delete(instance) + + @staticmethod + def replace(old_instance, new_instance): + """ + Replaces an instance of a model from the database with a new instance. + + :param old_instance: the instance to be replaced from the db. + :param new_instance: the new instance + """ + + global DBSession + + with transaction.manager: + DBSession.add(new_instance) + DBSession.delete(old_instance) + + @classmethod + def delete_all(cls): + """ + Deletes all instances of a model from the database. + """ + + global DBSession + + with transaction.manager: + deleted_rows = DBSession.query(cls).delete() + + return deleted_rows + + +Base = declarative_base(cls=ORMClass) # initialise a declarative Base instance to use for the web app models (inherits from the base ORM class defined above) class ServiceFunctionEndpoint(Base): @@ -17,12 +91,68 @@ class ServiceFunctionEndpoint(Base): __table_args__ = (UniqueConstraint('sf_i', 'sf_endpoint', 'sr'),) # defines a unique constraint across 3 columns - sf_i, sf_endpoint, sr - uid = Column(Integer, primary_key=True, autoincrement=True) # a primary key integer field (auto incremented) + uid = Column(Integer, primary_key=True, autoincrement=True, nullable=False) # a primary key integer field (auto incremented) + + location = Column(String, nullable=False) # cluster label + sfc = Column(String, nullable=False) # service function chain label + sfc_i = Column(String, nullable=False) # service function chain instance identifier + sf = Column(String, nullable=False) # service function label + sf_i = Column(String, nullable=False) # service function identifier (potentially FQDN) + sf_endpoint = Column(String, nullable=False) # service function endpoint (potentially IP address) + sr = Column(String, nullable=False) # service router ID - service router that connects the VM to FLAME + + @property + def json(self): + """ + Converts an instance of a ServiceFunctionEndpoint to JSON format. + + :return: a python dictionary object + """ + + fields = {c.name: getattr(self, c.name) for c in self.__table__.columns} + fields.pop("uid") + + 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 ServiceFunctionEndpoint.__table__.columns if column.name != "uid") + + @staticmethod + def constrained_columns(): + """ + :return: the columns that are uniquely identifying an instance of this model. + """ + + return tuple(column.name for column in ServiceFunctionEndpoint.__table_args__[0].columns) + + @staticmethod + def get(sf_i, sf_endpoint, sr): + """ + Gets the instance matching the unique constraint or None if not existing. + + :param sf_i: service function instance + :param sf_endpoint: service function endpoint + :param sr: service router + :return: the first object from the result set that matches the unique constraint or None + """ + + return ServiceFunctionEndpoint.query().filter(and_(ServiceFunctionEndpoint.sf_i == sf_i, ServiceFunctionEndpoint.sf_endpoint == sf_endpoint, ServiceFunctionEndpoint.sr == sr)).first() + + @staticmethod + def exists(sf_i, sf_endpoint, sr): + """ + Checks if an instance matching the unique constraint exists. + + :param sf_i: service function instance + :param sf_endpoint: service function endpoint + :param sr: service router + :return: True if exists, False otherwise + """ - location = Column(String) # cluster label - sfc = Column(String) # service function chain label - sfc_i = Column(String) # service function chain instance identifier - sf = Column(String) # service function label - sf_i = Column(String) # service function identifier (potentially FQDN) - sf_endpoint = Column(String) # service function endpoint (potentially IP address) - sr = Column(String) # service router ID - service router that connects the VM to FLAME + return ServiceFunctionEndpoint.get(sf_i, sf_endpoint, sr) is not None diff --git a/src/service/clmcservice/whoamiapi/conftest.py b/src/service/clmcservice/whoamiapi/conftest.py new file mode 100644 index 0000000..f6ddd2c --- /dev/null +++ b/src/service/clmcservice/whoamiapi/conftest.py @@ -0,0 +1,89 @@ +import pytest +from sqlalchemy import create_engine +from sqlalchemy.exc import ProgrammingError, OperationalError +from clmcservice.models import DBSession, Base + + +def create_test_database(db_name): + """ + This function creates a test database with the given name. If the database already exists, it is recreated. + + :param db_name: the test database name + """ + + engine = create_engine("postgresql://clmc:clmc_service@localhost:5432/postgres", echo=False) + conn = engine.connect().execution_options(autocommit=False) + conn.execute("ROLLBACK") # connection is already in a transaction, hence roll back (postgres databases cannot be created in a transaction) + try: + conn.execute("DROP DATABASE %s" % db_name) + print("\nOld database '{0}' has been deleted.".format(db_name)) + except ProgrammingError: + # database probably doesn't exist + conn.execute("ROLLBACK") + except OperationalError as e: + print(e) + # database exists and is probably being used by other users + conn.execute("ROLLBACK") + conn.close() + engine.dispose() + raise pytest.exit("Old test database cannot be deleted.") + + conn.execute("CREATE DATABASE %s" % db_name) + conn.close() + engine.dispose() + print("\nNew test database '{0}' has been created.".format(db_name)) + + +def initialise_database(db_name): + """ + This function initialises the test database by binding the shared DB session to a new connection engine and creating tables for all models. + + :param db_name: test database name + :return: the configured DB session, which is connected to the test database + """ + + engine = create_engine('postgresql://clmc:clmc_service@localhost:5432/{0}'.format(db_name)) # create an engine to connect to the test database + DBSession.configure(bind=engine) # configure the database session + Base.metadata.bind = engine + Base.metadata.create_all() # create tables for all models + + return DBSession, engine + + +def drop_test_database(db_name): + """ + This function removes the test database with the given name, if it exists + + :param db_name: the test database name + """ + + engine = create_engine("postgresql://clmc:clmc_service@localhost:5432/postgres", echo=False) + conn = engine.connect().execution_options(autocommit=False) + conn.execute("ROLLBACK") # connection is already in a transaction, hence roll back (postgres databases cannot be created in a transaction) + try: + conn.execute("DROP DATABASE %s" % db_name) + print("\nTest database '{0}' has been deleted.".format(db_name)) + except ProgrammingError: + # database probably doesn't exist + conn.execute("ROLLBACK") + except OperationalError as e: + print(e) + # database is probably being used by other users + conn.execute("ROLLBACK") + + conn.close() + engine.dispose() + + +@pytest.fixture(scope='module', autouse=True) +def testing_db_session(): + + test_database = "whoamitestdb" + 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 diff --git a/src/service/clmcservice/whoamiapi/tests.py b/src/service/clmcservice/whoamiapi/tests.py index e69de29..74e9379 100644 --- a/src/service/clmcservice/whoamiapi/tests.py +++ b/src/service/clmcservice/whoamiapi/tests.py @@ -0,0 +1,278 @@ +import pytest +from json import dumps +from pyramid import testing +from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPConflict +from clmcservice.models import ServiceFunctionEndpoint +from clmcservice.whoamiapi.views import WhoamiAPI + + +class TestWhoamiAPI(object): + """ + A pytest-implementation test for the WHOAMI API endpoints + """ + + @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() + ServiceFunctionEndpoint.delete_all() # clear the instances of the model in the test database + + def test_get_all(self): + """ + Tests the GET all method of the WHOAMI API - returns a list of all service function endpoint configurations from the database. + """ + + request = testing.DummyRequest() + response = WhoamiAPI(request).get_all() + assert response == [], "Initially there mustn't be any service function endpoint configurations in the database." + + sf_e = ServiceFunctionEndpoint(location="DC1", sfc="sfc1", sfc_i="sfc_i1", sf="sf1", sf_i="sf_i1", sf_endpoint="sf_endpoint1", sr="sr1") + expected_response_data = [sf_e.json] + ServiceFunctionEndpoint.add(sf_e) # adds the new instance of the model to the database + + request = testing.DummyRequest() + response = WhoamiAPI(request).get_all() + assert response == expected_response_data, "Incorrect response data with 1 service function endpoint configuration." + + sf_e = ServiceFunctionEndpoint(location="DC2", sfc="sfc2", sfc_i="sfc_i2", sf="sf2", sf_i="sf_i2", sf_endpoint="sf_endpoint2", sr="sr2") + expected_response_data.append(sf_e.json) + ServiceFunctionEndpoint.add(sf_e) + sf_e = ServiceFunctionEndpoint(location="DC3", sfc="sfc3", sfc_i="sfc_i3", sf="sf3", sf_i="sf_i3", sf_endpoint="sf_endpoint3", sr="sr3") + expected_response_data.append(sf_e.json) + ServiceFunctionEndpoint.add(sf_e) + + request = testing.DummyRequest() + response = WhoamiAPI(request).get_all() + assert response == expected_response_data, "Incorrect response data with more than 1 service function endpoint configurations." + + def test_get_one(self): + """ + Tests the GET one method of the WHOAMI API - returns an instance of a service function endpoint configuration from the database. + """ + + request = testing.DummyRequest() + response = WhoamiAPI(request).get_all() + assert response == [], "Initially there mustn't be any service function endpoint configurations in the database." + + self._validation_of_url_parameters_test("get_one") + + sf_e = ServiceFunctionEndpoint(location="DC1", sfc="sfc1", sfc_i="sfc_i1", sf="sf1", sf_i="sf_i1", sf_endpoint="sf_endpoint1", sr="sr1") + expected_response_data = sf_e.json + ServiceFunctionEndpoint.add(sf_e) # adds the new instance of the model to the database + + request = testing.DummyRequest() + request.params["sf_endpoint"] = "sf_endpoint1" + request.params["sf_i"] = "sf_i1" + request.params["sr"] = "sr1" + response = WhoamiAPI(request).get_one() + assert response == expected_response_data, "Invalid data returned in the response of GET instance" + + request = testing.DummyRequest() + request.params["sf_endpoint"] = "sf_endpoint2" + request.params["sf_i"] = "sf_i2" + request.params["sr"] = "sr2" + error_raised = False + try: + WhoamiAPI(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 endpoint" + + def test_post(self): + """ + Tests the POST method of the WHOAMI API - creates an instance of a service function endpoint configuration in the database. + """ + + request = testing.DummyRequest() + response = WhoamiAPI(request).get_all() + assert response == [], "Initially there mustn't be any service function endpoint configurations in the database." + + resource = dict(location="DC1", sfc="sfc1", sfc_i="sfc_i1", sf="sf1", sf_i="sf_i1", sf_endpoint="sf_endpoint1", sr="sr1") + json_data = dumps(resource) + request = testing.DummyRequest() + request.body = json_data.encode(request.charset) + response = WhoamiAPI(request).post() + assert response == resource, "POST request must return the created resource" + assert ServiceFunctionEndpoint.exists("sf_i1", "sf_endpoint1", "sr1"), "POST request must have created the resource" + + resource["location"] = "DC2" + json_data = dumps(resource) + request = testing.DummyRequest() + request.body = json_data.encode(request.charset) + error_raised = False + try: + WhoamiAPI(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", [ + ('{"location": "DC1", "sfc": "sfc1", "sfc_i": "sfc_i1", "sf": "sf1", "sf_i": "sf_i1", "sf_endpoint": "sf_endpoint1", "sr": "sr1"}', True), + ('{"location": "DC2", "sfc": "sfc2", "sfc_i": "sfc_i2", "sf": "sf2", "sf_i": "sf_i2", "sf_endpoint": "sf_endpoint2", "sr": "sr2"}', True), + ('{}', False), + ('{"location": "DC1", "sfc": "sfc1", "sfc_i": "sfc_i1", "sf": "sf1", "sf_i": "sf_i1"}', False), + ('{"place": "DC2", "sfc": "sfc2", "sfc_i": "sfc_i2", "sf": "sf2", "sf_i": "sf_i2", "sf_endpoint": "sf_endpoint2", "sr": "sr2"}', 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: + WhoamiAPI(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 WHOAMI API - overwrites an instance of a service function endpoint configuration from the database. + """ + + request = testing.DummyRequest() + response = WhoamiAPI(request).get_all() + assert response == [], "Initially there mustn't be any service function endpoint configurations in the database." + + self._validation_of_url_parameters_test("put") + + resource = dict(location="location1", sfc="sfc1", sfc_i="sfc_i1", sf="sf1", sf_i="sf_i1", sf_endpoint="sf_endpoint1", sr="sr1") + body = dumps(resource) + request = testing.DummyRequest() + request.params["sf_endpoint"] = "sf_endpoint1" + request.params["sf_i"] = "sf_i1" + request.params["sr"] = "sr1" + request.body = body.encode(request.charset) + error_raised = False + try: + WhoamiAPI(request).put() + except HTTPNotFound: + error_raised = True + assert error_raised, "Not found error must be raised in case of a non existing service function endpoint" + + sf_e = ServiceFunctionEndpoint(location="DC1", sfc="sfc1", sfc_i="sfc_i1", sf="sf1", sf_i="sf_i1", sf_endpoint="sf_endpoint1", sr="sr1") + ServiceFunctionEndpoint.add(sf_e) # adds the new instance of the model to the database + + resource = dict(location="location1", sfc="sfc1", sfc_i="sfc_i1", sf="sf1", sf_i="sf_i1", sf_endpoint="sf_endpoint1", sr="sr1") + body = dumps(resource) + request = testing.DummyRequest() + request.params["sf_endpoint"] = "sf_endpoint1" + request.params["sf_i"] = "sf_i1" + request.params["sr"] = "sr1" + request.body = body.encode(request.charset) + response = WhoamiAPI(request).put() + assert response == resource, "PUT request must return the updated resource" + assert ServiceFunctionEndpoint.get("sf_i1", "sf_endpoint1", "sr1").json["location"] == "location1" + + resource = dict(location="DC1", sfc="sfc1", sfc_i="sfc_i1", sf="sf1", sf_i="sf_i2", sf_endpoint="sf_endpoint2", sr="sr2") + body = dumps(resource) + request = testing.DummyRequest() + request.params["sf_endpoint"] = "sf_endpoint1" + request.params["sf_i"] = "sf_i1" + request.params["sr"] = "sr1" + request.body = body.encode(request.charset) + response = WhoamiAPI(request).put() + assert response == resource, "PUT request must return the updated resource" + assert not ServiceFunctionEndpoint.exists("sf_i1", "sf_endpoint1", "sr1"), "Resource has not been updated" + assert ServiceFunctionEndpoint.exists("sf_i2", "sf_endpoint2", "sr2"), "Resource has not been updated" + + sf_e = ServiceFunctionEndpoint(location="DC1", sfc="sfc1", sfc_i="sfc_i1", sf="sf1", sf_i="sf_i1", sf_endpoint="sf_endpoint1", sr="sr1") + ServiceFunctionEndpoint.add(sf_e) # adds the new instance of the model to the database + + resource = dict(location="DC1", sfc="sfc1", sfc_i="sfc_i1", sf="sf1", sf_i="sf_i2", sf_endpoint="sf_endpoint2", sr="sr2") + body = dumps(resource) + request = testing.DummyRequest() + request.params["sf_endpoint"] = "sf_endpoint1" + request.params["sf_i"] = "sf_i1" + request.params["sr"] = "sr1" + request.body = body.encode(request.charset) + error_raised = False + try: + WhoamiAPI(request).put() + except HTTPConflict: + error_raised = True + assert error_raised, "PUT request validates unique constraint" + + def test_delete(self): + """ + Tests the DELETE method of the WHOAMI API - deletes an instance of a service function endpoint configuration from the database. + """ + + request = testing.DummyRequest() + response = WhoamiAPI(request).get_all() + assert response == [], "Initially there mustn't be any service function endpoint configurations in the database." + + self._validation_of_url_parameters_test("delete") + + sf_e = ServiceFunctionEndpoint(location="DC1", sfc="sfc1", sfc_i="sfc_i1", sf="sf1", sf_i="sf_i1", sf_endpoint="sf_endpoint1", sr="sr1") + ServiceFunctionEndpoint.add(sf_e) # adds the new instance of the model to the database + + assert ServiceFunctionEndpoint.exists("sf_i1", "sf_endpoint1", "sr1") + + request = testing.DummyRequest() + request.params["sf_endpoint"] = "sf_endpoint1" + request.params["sf_i"] = "sf_i1" + request.params["sr"] = "sr1" + response = WhoamiAPI(request).delete() + assert response == {}, "DELETE must return an empty body if successful" + + assert not ServiceFunctionEndpoint.exists("sf_i1", "sf_endpoint1", "sr1"), "Resource must be deleted after the delete API method has been called." + + request = testing.DummyRequest() + request.params["sf_endpoint"] = "sf_endpoint1" + request.params["sf_i"] = "sf_i1" + request.params["sr"] = "sr1" + error_raised = False + try: + WhoamiAPI(request).delete() + except HTTPNotFound: + error_raised = True + assert error_raised, "Not found error must be raised in case of a non existing service function endpoint" + + @staticmethod + def _validation_of_url_parameters_test(method): + """ + Validates the way a whoami API method handles url query parameters + + :param method: the method to test + """ + + request = testing.DummyRequest() + error_raised = False + try: + getattr(WhoamiAPI(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["sf_i"] = "sf_i" + request.params["sr"] = "sr" + try: + getattr(WhoamiAPI(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_endp"] = "sf_endpoint" # argument should be sf_endpoint + request.params["sf_i"] = "sf_i" + request.params["sr"] = "sr" + try: + getattr(WhoamiAPI(request), method).__call__() + except HTTPBadRequest: + error_raised = True + assert error_raised, "Error must be raised in case of invalid naming of arguments" diff --git a/src/service/clmcservice/whoamiapi/utilities.py b/src/service/clmcservice/whoamiapi/utilities.py new file mode 100644 index 0000000..e75439e --- /dev/null +++ b/src/service/clmcservice/whoamiapi/utilities.py @@ -0,0 +1,71 @@ +#!/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 : 25-06-2018 +// Created for Project : FLAME +""" + +from json import loads +from clmcservice.models import ServiceFunctionEndpoint + + +def validate_sfendpoint_body(body): + """ + Validates the request body used to create an endpoint configuration resource in the database. + + :param body: the request body to validate + :return the validated configuration dictionary object + :raise AssertionError: if the body is not a valid configuration + """ + + try: + body = loads(body) + except: + raise AssertionError("Configuration must be a JSON object.") + + # the database table has one more column which is a UID integer + assert len(body) == len(ServiceFunctionEndpoint.__table__.columns) - 1, "Endpoint configuration 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 ServiceFunctionEndpoint.required_columns(): + assert attribute in body, "Required attribute not found in the request content." + + return body + + +def validate_sfendpoint_params(params): + """ + Validates the request parameters to retrieve an endpoint configuration resource from the database. + + :param params: the parameters dictionary to validate + :return: the validated parameters + :raise AssertionError: for invalid parameters + """ + + constrained_cols = ServiceFunctionEndpoint.constrained_columns() + + assert len(params) == len(constrained_cols), "Incorrect number of arguments." + + # validate that all required attributes are given in the dictionary + for attribute in constrained_cols: + assert attribute in params, "Required attribute not found in the request parameters." + + return params + diff --git a/src/service/clmcservice/whoamiapi/views.py b/src/service/clmcservice/whoamiapi/views.py index e69de29..6ac5c15 100644 --- a/src/service/clmcservice/whoamiapi/views.py +++ b/src/service/clmcservice/whoamiapi/views.py @@ -0,0 +1,191 @@ +#!/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 : 25-06-2018 +// Created for Project : FLAME +""" + +from pyramid.httpexceptions import HTTPBadRequest, HTTPConflict, HTTPNotFound +from pyramid.view import view_defaults, view_config +from clmcservice.models import ServiceFunctionEndpoint +from clmcservice.whoamiapi.utilities import validate_sfendpoint_body, validate_sfendpoint_params + + +@view_defaults(renderer='json') +class WhoamiAPI(object): + """ + A class-based view for accessing and mutating the configuration of SF endpoints - namely, the WHOAMI API. + """ + + 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='whoami_endpoints', request_method='GET') + def get_all(self): + """ + GET API call for all resources. + + :return: A list of all service function endpoint configurations found in the database. + """ + + return [instance.json for instance in ServiceFunctionEndpoint.query()] + + @view_config(route_name='whoami_endpoints_instance', request_method='GET') + def get_one(self): + """ + GET API call for a single resources. + + :return: One service function endpoint configuration instance retrieved from the database by querying the uniquely constrained columns. + :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_endpoint = self._get_sf_endpoint_from_url_string() + if sf_endpoint is None: + raise HTTPNotFound("A service function endpoint with the given parameters doesn't exist.") + else: + return sf_endpoint.json + + @view_config(route_name='whoami_endpoints', request_method='POST') + def post(self): + """ + A POST API call to create a new service function endpoint. + + :return: A JSON response to the POST call - essentially with the new configured data and comment of the state of the aggregator + :raises HTTPBadRequest: if request body is not a valid JSON for the configuration + :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_endpoint = self._validate_and_create() + json_data = sf_endpoint.json + ServiceFunctionEndpoint.add(sf_endpoint) + + self.request.response.status = 201 + + return json_data + + @view_config(route_name='whoami_endpoints_instance', request_method='PUT') + def put(self): + """ + A PUT API call to update a service function endpoint. + + :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_endpoint = self._get_sf_endpoint_from_url_string() + if sf_endpoint is None: + raise HTTPNotFound("A service function endpoint with the given parameters doesn't exist.") + else: + try: + body = self.request.body.decode(self.request.charset) + validated_body = validate_sfendpoint_body(body) # validate the content and receive a json dictionary object + except AssertionError as e: + raise HTTPBadRequest("Bad request content. Configuration format is incorrect: {0}".format(e.args)) + + new_resource = validated_body + old_resource = sf_endpoint.json + updating = new_resource["sf_i"] == old_resource["sf_i"] and new_resource["sf_endpoint"] == old_resource["sf_endpoint"] and new_resource["sr"] == old_resource["sr"] + + if updating: + ServiceFunctionEndpoint.delete(sf_endpoint) + new_sf_endpoint = ServiceFunctionEndpoint(**validated_body) + ServiceFunctionEndpoint.add(new_sf_endpoint) + else: + resource_exists = ServiceFunctionEndpoint.exists(new_resource["sf_i"], new_resource["sf_endpoint"], new_resource["sr"]) + if resource_exists: + raise HTTPConflict("Service function endpoint with this configuration already exists.") # error 409 in case of resource conflict + + new_sf_endpoint = ServiceFunctionEndpoint(**validated_body) + ServiceFunctionEndpoint.replace(sf_endpoint, new_sf_endpoint) + + return validated_body + + @view_config(route_name='whoami_endpoints_instance', request_method='DELETE') + def delete(self): + """ + Deletes an instance of a service function endpoint configuration in the database. + + :return: An empty body indicating the the content has been deleted - status code 204. + :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_endpoint = self._get_sf_endpoint_from_url_string() + if sf_endpoint is None: + raise HTTPNotFound("A service function endpoint with the given parameters doesn't exist.") + else: + ServiceFunctionEndpoint.delete(sf_endpoint) + self.request.response.status = 204 + return {} + + def _get_sf_endpoint_from_url_string(self): + """ + Retrieves a service function endpoint configuration from the database by validating and then using the request url parameters. + + :return: An instance of a service function endpoint configuration or None if not existing + """ + + params = {} + for attribute in ServiceFunctionEndpoint.constrained_columns(): + if attribute in self.request.params: + params[attribute] = self.request.params.get(attribute) + + try: + params = validate_sfendpoint_params(params) + except AssertionError as e: + raise HTTPBadRequest("Request format is incorrect: {0}".format(e.args)) + + sf_endpoint = ServiceFunctionEndpoint.get(**params) + return sf_endpoint + + 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 configuration + :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_sfendpoint_body(body) # validate the content and receive a json dictionary object + except AssertionError as e: + raise HTTPBadRequest("Bad request content. Configuration format is incorrect: {0}".format(e.args)) + + resource = validated_body + + resource_exists = ServiceFunctionEndpoint.exists(resource["sf_i"], resource["sf_endpoint"], resource["sr"]) + if resource_exists: + raise HTTPConflict("Service function endpoint with this configuration already exists.") # error 409 in case of resource conflict + + # create an instance of the model + sf_endpoint = ServiceFunctionEndpoint(**resource) + + return sf_endpoint -- GitLab